本文针对头条实习面试历年的面经归纳总结。

Java 基础

堆与栈的区别

Java 把内存划分成两种:一种是堆内存,一种是栈内存。

堆:主要用于存储实例化的对象,数组。由 JVM 动态分配内存空间。一个 JVM 只有一个堆内存,线程是可以共享数据的。

栈:主要用于存储局部变量和对象的引用变量,每个线程都会有一个独立的栈空间,所以线程之间是不共享数据的。

volatile, synchronized 关键字

volatile,它能够使变量在值发生改变时能尽快地让其他线程知道,保证了数据的可见性。

synchronized,它可作用于一段代码或方法,既可以保证可见性,又能够保证原子性。

可见性体现在:通过 synchronized 或者 Lock 能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存中。

原子性表现在:要么不执行,要么执行到底。

Hashmap

简单来说,HashMap 由数组 + 链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前 entry 的 next 指向 null), 那么对于查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度为 O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过 key 对象的 equals 方法逐一比对查找。所以,性能考虑,HashMap 中的链表出现越少,性能才会越好。

Hash 冲突

根据 key 即经过一个函数 f(key) 得到的结果的作为地址去存放当前的 key value 键值对 (这个是 hashmap 的存值方式),但是却发现算出来的地址上已经有人先来了。就是说这个地方被抢了啦。这就是所谓的 hash 冲突啦。

Hash 不是线程安全。

Java Clone复制对象

浅拷贝:被复制对象的所有值属性都含有与原来对象的相同,而所有的对象引用属性仍然指向原来的对象。

深拷贝:在浅拷贝的基础上,所有引用其他对象的变量也进行了 clone,并指向被复制过的新对象。

Java 的三大特性:多态是怎么实现的

Java 提供了编译时多态和运行时多态两种多态机制。前者是通过方法重载(变量相关)实现的,后者是通过方法的重写(继承相关)实现的。

ArrayList/LinkedList 实现对比,使用场景

ArrayList 和 LinkedList 的大致区别如下:

  1. ArrayList 是实现了基于动态数组的数据结构,LinkedList 基于链表的数据结构。
  2. 对于随机访问 get 和 set,ArrayList 觉得优于 LinkedList,因为 LinkedList 要移动指针。
  3. 对于新增和删除操作 add 和 remove,LinedList 比较占优势,因为 ArrayList 要移动数据。

int 和 Integer 区别

1、Integer 是 int 的包装类,int 则是 java 的一种基本数据类型。
2、Integer 变量必须实例化后才能使用,而 int 变量不需要。
3、Integer 实际是对象的引用,当 new 一个 Integer 时,实际上是生成一个指针指向此对象;而 int 则是直接存储数据值。
4、Integer 的默认值是 null,int 的默认值是 0。

JVM

JVM原理

JVM本身是介于JAVA编译器和操作系统之间的程序,这个程序提供了一个无视操作系统和硬件平台的运行环境

Java内存区域的分配

以1.8为例,内存区域如下:

按线程是否共享分为以下区域:

所有线程共享的数据区:

  1. 方法区: 存储已被虚拟机加载的类信息、方法信息、常量、静态变量、字节码、JIT编译后的本地代码,并使用永久代来实现方法区。1.8中用元空间替代了永久代,元空间并不在虚拟机中,而是使用本地内存,元空间中可能还存在短指针数据区CCS
  2. 堆区: 最大的一块区域,用于存放对象的区域,1.7之后常量池移到这里

每个线程都会有一块私有的数据区:

  1. 虚拟机栈: 每个方法执行时在其中创建一个栈帧,用于存储局部变量、操作数栈、动态链接、方法出口等信息
  2. 本地方法栈: 功能与虚拟机栈相同,为native方法服务
  3. 程序计数器: 存放当前正在执行的指令的地址

直接内存:

  • 直接内存并非Java标准。
  • JDK1.4 加入了新的 NIO 机制,目的是防止 Java 堆 和 Native 堆之间往复的数据复制带来的性能损耗,此后 NIO 可以使用 Native 的方式直接在 Native 堆分配内存。
  • 直接内存区域是全局共享的内存区域。

类加载机制

初始化时机

new、静态字段或方法被使用、反射、父类、main函数调用

加载过程

  1. 加载(获取字节流并转换成运行时数据结构,然后生成Class对象)
  2. 验证(验证字节流信息符合当前虚拟机的要求)
  3. 准备(为类变量分配内存并设置初始值)
  4. 解析(将常量池的符号引用替换为直接引用)
  5. 初始化(执行类构造器-类变量赋值和静态块的过程)

GC回收机制

回收对象

不可达对象:通过一系列的GC Roots的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时则此对象是不可用的。
GC Roots包括:虚拟机栈中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中JNI(Native方法)引用的对象。

彻底死亡条件:
条件1:通过GC Roots作为起点的向下搜索形成引用链,没有搜到该对象,这是第一次标记。
条件2:在finalize方法中没有逃脱回收(将自身被其他对象引用),这是第一次标记的清理。

如何回收

新生代因为每次GC都有大批对象死去,只需要付出少量存活对象的复制成本且无碎片所以使用“复制算法”
老年代因为存活率高、没有分配担保空间,所以使用“标记-清理”或者“标记-整理”算法

复制算法:将可用内存按容量划分为Eden、from survivor、to survivor,分配的时候使用Eden和一个survivor,Minor GC后将存活的对象复制到另一个survivor,然后将原来已使用的内存一次清理掉。这样没有内存碎片。
标记-清除:首先标记出所有需要回收的对象,标记完成后统一回收被标记的对象。会产生大量碎片,导致无法分配大对象从而导致频繁GC。
标记-整理:首先标记出所有需要回收的对象,让所有存活的对象向一端移动。

Minor GC条件

当Eden区空间不足以继续分配对象,发起Minor GC。

GC 算法

GC 最基础的算法有三种: 标记 - 清除算法、复制算法、标记 - 压缩算法,我们常用的垃圾回收器一般都采用分代收集算法:

  • 标记 - 清除算法,“标记 - 清除”(Mark-Sweep)算法,如它的名字一样,算法分为 “标记” 和 “清除” 两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。
  • 复制算法,“复制”(Copying)的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
  • 标记 - 压缩算法,标记过程仍然与 “标记 - 清除” 算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
  • 分代收集算法,“分代收集”(Generational Collection)算法,把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。

架构

进程线程,通信方式,同步方式

线程通信

  • 使用全局变量
  • 使用消息实现通信
  • 使用事件 CEvent 类实现线程间通信

进程通信

1、低级通信, 控制信息的通信 (主要用于进程之间的同步, 互斥, 终止和挂起等等控制信息的传递)。

2、高级通信, 大批数据信息的通信 (主要用于进程间数据块数据的交换和共享, 常见的高级通信有管道, 消息队列, 共享内存等)。

线程同步

  • 临界区

临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。在任意时刻只允许一个线程对共享资源进行访问,如果有多个线程试图访问公共资源,那么在有一个线程进入后,其他试图访问公共资源的线程将被挂起,并一直等到进入临界区的线程离开,临界区在被释放后,其他线程才可以抢占。

  • 互斥量

互斥与临界区很相似,但是使用时相对复杂一些(互斥量为内核对象),不仅可以在同一应用程序的线程间实现同步,还可以在不同的进程间实现同步,从而实现资源的安全共享。
PS:1、互斥量由于也有线程所有权的概念,故也只能进行线程间的资源互斥访问,不能由于线程同步;
2、由于互斥量是内核对象,因此其可以进行进程间通信,同时还具有一个很好的特性,就是在进程间通信时完美的解决了” 遗弃” 问题

  • 信号量

信号量的用法和互斥的用法很相似,不同的是它可以同一时刻允许多个线程访问同一个资源,PV 操作。
PS: 事件可以完美解决线程间的同步问题,同时信号量也属于内核对象,可用于进程间的通信

  • 事件

事件分为手动置位事件和自动置位事件。事件 Event 内部它包含一个使用计数(所有内核对象都有),一个布尔值表示是手动置位事件还是自动置位事件,另一个布尔值用来表示事件有无触发。由 SetEvent() 来触发,由 ResetEvent() 来设成未触发。
PS: 事件是内核对象, 可以解决线程间同步问题,因此也能解决互斥问题。

Dubbo

Apache Dubbo (incubating) |ˈdʌbəʊ| 是一款高性能、轻量级的开源 Java RPC 框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。简单来说 Dubbo 是一个分布式服务框架,致力于提供高性能和透明化的 RPC 远程服务调用方案,以及 SOA 服务治理方案。

RPC

正如上一讲所说,RPC 主要是为了解决的两个问题:

  • 解决分布式系统中,服务之间的调用问题。

  • 远程调用时,要能够像本地调用一样方便,让调用者感知不到远程调用的逻辑。

异步和同步

同步:执行一个操作之后,等待结果,然后才继续执行后续的操作。

异步:执行一个操作后,可以去执行其他的操作,然后等待通知再回来执行刚才没执行完的操作。

阻塞:进程给 CPU 传达一个任务之后,一直等待 CPU 处理完成,然后才执行后面的操作。

非阻塞:进程给 CPU 传达任我后,继续处理后续的操作,隔断时间再来询问之前的操作是否完成。这样的过程其实也叫轮询。

操作系统

协程

协程与异步结合——性能与简单的结合
结合上面的分析,如果我们可以写下面功能的代码,将很完美:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void start()
{
for(;;)
{
Buffer buf;
yeild readAsync(buf,start);
//------ 分隔线,协程在这里返回,等待 readAsync 完成,当 readAsync 完成后,它再调用 start
// 此时 start 将从这里接着运行
yeild writeAsync(buf, start);
//------ 分隔线,协程在这里返回,等待 writeAsync 完成,当 writeAsync 完成后,它再调用 start
// 此时 start 将从这里接着运行
}
}

上面的代码也很一清晰明了。如果真的能写这样的代码,那将是所有程序员之福。这样在一些语言里确实可以直接写出来,比如 Python、Lua、 golang,这些语言有原生的协程支持——编译器或解释器为我们处理了这些执行流的跳转。那么在 C/C++ 里怎么实现呢?可能肯定的是,C/C++ 里不能直接写出这样简洁的代码,但却可以写类似的代码,尤其是 C++——它提供了更强大的封装能力。

内存管理

内存管理包括内存管理概念、交换与覆盖、连续分配管理方式和非连续分配管理方式(分页管理方式、分段管理方式、段页式管理方式)。

虚拟内存管理包括虚拟内存概念、请求分页管理方式、页面置换算法、页面分配策略、工作集和抖动。

内存管理的功能有:
内存空间的分配与回收:由操作系统完成主存储器空间的分配和管理,使程序员摆脱存储分配的麻烦,提高编程效率。

地址转换:在多道程序环境下,程序中的逻辑地址与内存中的物理地址不可能一致,因此存储管理必须提供地址变换功能,把逻辑地址转换成相应的物理地址。

内存空间的扩充:利用虚拟存储技术或自动覆盖技术,从逻辑上扩充内存。
存储保护:保证各道作业在各自的存储空间内运行,. 互不干扰。

死锁是啥,死锁产生的条件是什么

所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。

产生的原因:

  • 竞争资源
  • 进程间推进顺序非法

wait(), notify()/notifyAll()

  • wait(): 使当前线程阻塞

  • notify()/notifyAll(): 唤醒线程

正在 wait 的线程被唤醒后会发生什么

而实际情况是,被唤醒并且被执行的线程是从上次阻塞的位置从下开始运行,也就是从 wait() 方法后开始执行。

notifyAll 唤醒所有的等待线程后会发生什么

notifyAll 是针对指定对象里面的所有线程执行唤醒操作,指定对象一旦唤醒成功。则会立即加入线程的资源争夺中去。

Linux 命令 如何杀死占用指定端口的进程

lsof : 端口号 # 查看占用端口的进程
kill -9 进程号 # kill 掉该进程

线程之间共享数据的方式

  1. 多个线程对共享数据的操作是相同的,那么创建一个 Runnable 的子类对象,将这个对象作为参数传递给 Thread 的构造方法,此时因为多个线程操作的是同一个 Runnable 的子类对象,所以他们操作的是同一个共享数据。

  2. 多个线程对共享数据的操作是不同的,将共享数据和操作共享数据的方法放在同一对象中,将这个对象作为参数传递给 Runnable 的子类,在子类中用该对象的方法对共享数据进行操作。如:生产者消费则。

  3. 多个线程对共享数据的操作是不同的, 用内部类的方式去实现,创建 Runnable 的子类作为内部类,将共享对象作为全局变量,在 Runnable 的子类中对共享数据进行操作。

进程和线程的区别

进程是资源分配的最小单位,线程是程序执行的最小单位。

进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。而线程是共享进程中的数据的,使用相同的地址空间,因此 CPU 切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。

线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC) 进行。不过如何处理好同步与互斥是编写多线程程序的难点。

但是多进程程序更健壮,多线程程序只要有一个线程死掉,整个进程也死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间。

数据库

数据库事务隔离级别

Read uncommitted – 不防止任何隔离性问题, 具有脏读 / 不可重复度 / 虚读 (幻读) 问题

Read committed – 可以防止脏读问题, 但是不能防止不可重复度 / 虚读 (幻读) 问题

Repeatable read – 可以防止脏读 / 不可重复读问题, 但是不能防止虚读 (幻读) 问题

Serializable – 数据库被设计为单线程数据库, 可以防止上述所有问题

数据库的 ACID

⑴ 原子性(Atomicity)

原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚,这和前面两篇博客介绍事务的功能是一样的概念,因此事务的操作如果成功就必须要完全应用到数据库,如果操作失败则不能对数据库有任何影响。

⑵ 一致性(Consistency)

一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。

拿转账来说,假设用户 A 和用户 B 两者的钱加起来一共是 5000,那么不管 A 和 B 之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是 5000,这就是事务的一致性。

⑶ 隔离性(Isolation)

隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。

即要达到这么一种效果:对于任意两个并发的事务 T1 和 T2,在事务 T1 看来,T2 要么在 T1 开始之前就已经结束,要么在 T1 结束之后才开始,这样每个事务都感觉不到有其他事务在并发地执行。

关于事务的隔离性数据库提供了多种隔离级别,稍后会介绍到。

⑷ 持久性(Durability)
  
持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。

例如我们在使用 JDBC 操作数据库时,在提交事务方法后,提示用户事务操作完成,当我们程序执行完成直到看到提示后,就可以认定事务以及正确提交,即使这时候数据库出现了问题,也必须要将我们的事务完全执行完成,否则就会造成我们看到提示事务处理完毕,但是数据库因为故障而没有执行事务的重大错误。

数据库乐观锁悲观锁,以及乐观锁如何实现

在关系数据库管理系统里,悲观并发控制(又名 “悲观锁”,Pessimistic Concurrency Control,缩写 “PCC”)是一种并发控制的方法。它可以阻止一个事务以影响其他用户的方式来修改数据。如果一个事务执行的操作都某行数据应用了锁,那只有当这个事务把锁释放,其他事务才能够执行与该锁冲突的操作。

在关系数据库管理系统里,乐观并发控制(又名 “乐观锁”,Optimistic Concurrency Control,缩写 “OCC”)是一种并发控制的方法。它假设多用户并发的事务在处理时不会彼此互相影响,各事务能够在不产生锁的情况下处理各自影响的那部分数据。在提交数据更新之前## ,每个事务会先检查在该事务读取数据后,有没有其他事务又修改了该数据。如果其他事务有更新的话,正在提交的事务会进行回滚。

数据库索引

索引是对数据库表中一列或多列的值进行排序的一种结构,使用索引可快速访问数据库表中的特定信息。如果想按特定职员的姓来查找他或她,则与在表中搜索所有的行相比,索引有助于更快地获取信息。

为什么要用B+树,B+树和B树的区别

B 树(B-tree)是一种树状数据结构,它能够存储数据、对其进行排序并允许以 O(log n) 的时间复杂度运行进行查找、顺序读取、插入和删除的数据结构。

B+ 树是对 B 树的一种变形树,它与 B 树的差异在于:

有 k 个子结点的结点必然有 k 个关键码;
非叶结点仅具有索引作用,跟记录有关的信息均存放在叶结点中。
树的所有叶结点构成一个有序链表,可以按照关键码排序的次序遍历全部记录。

联合索引,最左匹配

mysql 建立多列索引(联合索引)有最左前缀的原则,即最左优先,如:

如果有一个 2 列的索引 (col1,col2), 则已经对(col1)、(col1,col2) 上建立了索引;
如果有一个 3 列索引 (col1,col2,col3),则已经对(col1)、(col1,col2)、(col1,col2,col3) 上建立了索引;

总结:
1、b + 树的数据项是复合的数据结构,比如 (name,age,sex) 的时候,b + 树是按照从左到右的顺序来建立搜索树的,比如当 (张三, 20,F) 这样的数据来检索的时候,b + 树会优先比较 name 来确定下一步的所搜方向,如果 name 相同再依次比较 age 和 sex,最后得到检索的数据;但当 (20,F) 这样的没有 name 的数据来的时候,b + 树就不知道第一步该查哪个节点,因为建立搜索树的时候 name 就是第一个比较因子,必须要先根据 name 来搜索才能知道下一步去哪里查询。

2、比如当 (张三, F) 这样的数据来检索时,b + 树可以用 name 来指定搜索方向,但下一个字段 age 的缺失,所以只能把名字等于张三的数据都找到,然后再匹配性别是 F 的数据了, 这个是非常重要的性质,即索引的最左匹配特性。(这种情况无法用到联合索引)

什么是红黑树

红黑树(Red Black Tree) 是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,典型的用途是实现关联数组。

网络与服务器

参照网络原理基础知识一文

数据结构与算法

  • 最大连续子数组和

思路,动态规划,状态转移方程: DP[i] = max{DP[i-1] + A[i],A[i]};

  • 一个先升后降的整数数组,找出最大的数

二分一次找到中点,如果中点 Array[mid] 比 Array[mid-1] 和 Array[mid+1] 都大,那么就返回 mid。否则,如果 Array[mid]>Array[mid-1], 则最大值在 mid 右边,如果

Array[mid]>Array[mid+1], 则最大值在 mid 左边。不断进行下去,直到找到为止。

LRU Cache

  • 算法题:二叉树最小公共祖先【一开始问的是一棵树里最远的两个节点间的距离,思考的有点久于是换题了…】
  1. 二叉搜索树情况,根据二叉搜索树的特征,从根节点开始查找,若两节点的 val 值都小于当前节点,则他们的最小公共祖先就去左子树找,若两节点的 val 值都大于当前节点,则他们的最小公共祖先就去右子树找。直到一个节点的值小于当前节点,另一个节点的值大于当前节点,那么当前节点为最小公共祖先,即为找到了可以返回了。

  2. 二叉树有指向父节点的指针:当二叉树中有指向父节点的指针时,可以倒过来看待这个问题,即为求两个链表的公共节点。从当前节点开始往根节点数,记录总共有多少个节点,记下数字。然后大数字的链表上先走几步到数字一样,再两节点都一起往根节点走,第一个相同的节点即为最小公共子节点。

  3. 简单直接一点。当 root 非空时,不断从其左右子树搜索,①如果两节点都在其左子树,对其左子树节点递归搜索;②如果两节点都在其右子树,对其右子树节点递归搜索;③如果一个节点在左子树一个节点在右子树,则当前节点即为最小公共祖先。

  • 算法题:链表成环判断

设置Run和Walk,如果run=walk则返回

  • 算法题:链表交叉判断

交叉只可能是Y型,记录长度,先让长的节点往后移动比短的节点夺得位置

  • 算法题:二叉树层级遍历,以及 follow up 是每下一层遍历方向颠倒
1
2
3
4
5
6
7
8
9
10
11
while (!queue.isEmpty ()) {
int size = queue.size ();
LinkedList<Integer> list = new LinkedList <> ( );
for(int i = 0; i< size; i++) {
TreeNode node = queue.pop ();
list.add ( node.val );
if(node.left != null) queue.offer ( node.left );
if(node.right != null) queue.offer ( node.right );
}
result.add(list);
}
  • 单链表奇数位递增,偶数位递减进行排序
  1. 首先根据奇数位和偶数位拆分成两个链表。
  2. 然后对偶数链表进行反转。
  3. 最后将两个有序链表进行合并。
  • 一个矩阵从左到右增从上到下增查找某个数,时间复杂度o(n)

  • top k问题堆排序

1.从 20 亿个数字的文本中,找出最大的前 100 个。

最直观:小顶堆(大顶堆 -> 最小 100 个数);
较高效:Quick Select 算法。

  • 最长递增子序列

DP

  • 01背包

optimal[i][j] = max(optimal[i-1][j],optimal[i-1][j-weight[i]]+value[i]);

  • 最长公共子串

  • 二叉树序列化和反序列化

  • 二叉树知道前序遍历和中序遍历构造还原

  • LRU算法实现不许用现成的数据结构

  • 一个货物八天的价格给出问哪天买哪天卖收益最大,时间复杂度o(n),要是可以买卖多次呢?

  • 下一个大的数字 1423->1432

  • 算法最长递增子序列的个数

  • 算法: 树的最长路径(把点存起来)

  • 给 n 个数,求这 n 个数组成的集合的所有子集

  • 平面上有 n 个点,求最多有多少个点在同一条直线上

  • 给 n 个数,求最大的区间和

  • 给 n 个非降序的数和一个数 k,求出 k 在 n 个数中第一次出现的位置

  • 有两个 TB 级大文件,一个文件每行为 id : name,另一个文件每行为 id : age,要求合并成一个文件 id : name, age (说思路不用写代码)

  • 给一个 01 矩阵,求出全为 1 的最大矩形位置和面积

实践

秒杀如何实现?

(1)将请求尽量拦截在系统上游(不要让锁冲突落到数据库上去)。传统秒杀系统之所以挂,请求都压倒了后端数据层,数据读写锁冲突严重,并发高响应慢,几乎所有请求都超时,流量虽大,下单成功的有效流量甚小。以 12306 为例,一趟火车其实只有 2000 张票,200w 个人来买,基本没有人能买成功,请求有效率为 0。

(2)充分利用缓存,秒杀买票,这是一个典型的读多写少的应用场景,大部分请求是车次查询,票查询,下单和支付才是写请求。一趟火车其实只有 2000 张票,200w 个人来买,最多 2000 个人下单成功,其他人都是查询库存,写比例只有 0.1%,读比例占 99.9%,非常适合使用缓存来优化。好,后续讲讲怎么个 “将请求尽量拦截在系统上游” 法,以及怎么个 “缓存” 法,讲讲细节。

其它

Java 的学习路线,看了什么书

我的面试

头条的面试比我想象的要简单一些,题目基本上都遇到过了,但自己准备得不太充分,导致面试结果很差,所以好好准备对面试还是很有帮助的。

复盘

非代码类

操作系统概述

进程与线程的区别

进程之间如何通信

  1. 管道:速度慢,容量有限,只有父子进程能通讯。

  2. FIFO:任何进程间都能通讯,但速度慢。

  3. 消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题。

  4. 信号量:不能传递复杂消息,只能用来同步。

  5. 共享内存区:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全,当然,共享内存区同样可以用作线程间通讯,不过没这个必要,线程间本来就已经共享了同一进程内的一块内存。

在CMD中按下 Ctrl+c 会出现什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
AttachConsole(PID); // attach to process console
SetConsoleCtrlHandler(NULL, TRUE); // disable Control+C handling for our app
GenerateConsoleCtrlEvent(CTRL_C_EVENT, 0); // generate Control+C event
HANDLE hProcess = OpenProcess(PROCESS_TERMINATE, FALSE, PID);
if (INVALID_HANDLE_VALUE == hProcess)
{
return ;
}
WaitForSingleObject(hProcess, INFINITE);
CloseHandle(hProcess);
Sleep(2000); //等待2秒,以防止自身被关闭。
FreeConsole();
SetConsoleCtrlHandler(NULL, FALSE);
}

网络原理

三次握手,四次挥手

如果不进行第四次挥手,会带来什么问题

数据库原理

介绍数据的 ACID 四个特性

数据库如何实现索引

介绍一下 InnoDB

代码类

  1. 手写两链表相加 1 -> 2 -> 3 -> 4 + 2 -> 3 -> 4 = 1 -> 4 -> 6 -> 8

  2. 写两线程分别是 [1,3,5,7] [2,4,6,8] 交替打印出, [1,2,3,4,5,6,7,8]

自我总结

我有4个问题,

  1. 表达问题,无论是自我介绍还是在回答问题时,表述十分不流利。

  2. 项目问题,项目介绍不自信。

  3. 题目问题,题目不能保证90%的回答。

  4. 手写题目,手写代码能力偏弱。

如何解决

针对以上四个问题,我应该这么去解决

  1. 准备面试不仅要看懂原理,也应该学会如何去表达,尝试复述问题的答案。

  2. 项目没有吃透,我应该围绕着我写的项目去搞懂里面的知识。

  3. 多留一些时间去准备面试问题,对症下药很重要。

  4. 我要把Leetcode里重点题目写得滚瓜烂熟。