⼩⽶⼀⾯(50 分钟+)
⼩⽶ Java ⾯试
最近,⼩⽶ 25 届秋招软件开发岗位的薪资已经开奖了。
不少同学看到开奖薪资之后都直⾔,⼩⽶⼿机有 性价⽐,没想到⼯资开的也有性价⽐,直接泪崩
了。
我从⽹上爆料同学的反馈,收集了⼩⽶软开的校招薪资开奖情况,⼩⽶研发⼆线城市主要是分布
在武汉和南京,⼀线城市是分布在北京、深圳、上海,不同城市的薪资也有⼀些差距,毕竟⼀⼆
线城市的⽣活开销还是不⼀样的。

武汉:11k x 15 = 16.5w 、15k x 15 = 22.5w 、18k x 15 = 27w
南京:12k x 15 = 18w 、15k x 15 = 22.5w
北京:18k x 15 = 27w整体来看,年薪是在 16w 〜30w 之间,中位数是 20w+ 年薪,跟去年基本⼀样。⼩⽶薪资相⽐互联
⽹⼤⼚差距还是⽐较明显 的,⽐如美团今年的⽩菜档 是 23k x 15.5 ,ssp offer 都达到 30k x 15.5
了,实在是⾹。
针对⼩⽶公司的开发岗位,我⽬前还没有看到有⼈爆料超过 20k+ 的,不过听到⼀些消息说,⼩⽶
开发岗位的 ssp offer 是 24k ,去年的⼩⽶ ssp 薪资确实就是 24k ,今年的话,⼤概率也是的。
虽然相⽐互联⽹⼀线⼤⼚是少了⼀些,但是也属于⼆线梯队的⼤⼚了。
⼩⽶的⾯试压⼒确实⽐互联⽹⼤⼚⼩了很多,互联⽹⼤⼚⼀场⾯试都是持续 1 ⼩时,问 20-30 个
⾯试题,⽽⼩⽶⼀场⾯试⾯试问题⼀般就 10-15 个,当然也会有⼿撕算法环节,所以要冲中⼤⼚
的同学,算法是逃不了 的,必须多练。
对了,有⼀些同学反馈,⼩⽶⾯试是在⻜书上 ⾯进⾏的,写代码全都是ACM 模式,⼒扣刷多的同
学可以练⼀下ACM 模式,不然到时候写链表都吭哧吭哧的。
这次,跟⼤家分享今 年** ⼩⽶秋招 Java 开发的⾯经,主要拷打了 8 个⼋股 + 2 个算法题

⼩⽶⼀⾯(50 分钟+)
Kafka 和RocketMQ 的区别?
Kafka 的优缺点:
优点:⾸先,Kafka 的最⼤优势就在于它的⾼吞吐 量,在普通机器4CPU8G 的配置下,⼀台机器
可以抗住⼗⼏万的QPS ,这⼀点还是相当优越的。Kafka ⽀持集群部署,如果部分机器宕机不可
⽤,则不影响Kafka 的正常使⽤。
缺点:Kafka 有可能会造成数据丢失,因为它在收到消息的时候,并不是直接写到物理磁盘的 ,
⽽是先写 ⼊到磁盘缓冲区⾥⾯的。Kafka 功能⽐较的单⼀主要的就是⽀持收发消息,⾼级功能基
本没有,就会造成适⽤场景受限。
RocketMQ 是阿⾥巴巴 开源的消息中间件,优缺点:
优点:⽀持功能⽐较多,⽐如延迟队列、消息事务等等 ,吞吐 量也⾼,单机吞吐 量达到 10 万
级,⽀持⼤规模集群部署,线性扩展⽅便,Java 语⾔开发,满⾜了国内绝⼤部分公司技术栈
缺点:性能相⽐ kafka 是弱⼀点,因为 kafka ⽤到了 sendfile 的零拷⻉技术,⽽ RocketMQ 主
要是⽤ mmap+write 来实现零拷⻉。
该怎么选择呢?
如果我们业务只是收发消息这种单⼀类型的需求,⽽且可以允许⼩部分数据丢失的可能性,但
是⼜要求极⾼的吞吐 量和⾼性能的话,就直接选Kafka 就⾏了,就好⽐我们公司想要收集和传输
⽤⼾⾏为⽇志以及其他相关⽇志的处理,就选⽤的Kafka 中间件。
如果公司的需要通过 mq 来实现⼀些业 务需求,⽐如延迟队列、消息事务等,公司技术栈主要
是Java 语⾔的话,就直接⼀步到位选择RocketMQ ,这样会省很多事情。
反射是什么 ?有什么 作⽤?
Java 反射机制是在运⾏状态中,对于任意⼀个类,都能够知道这个类中的所有属性和⽅法,对于
任意⼀个对象,都能够调⽤它的任意⼀个⽅法和属性;这种动态获取的信息以及动态调⽤对象的
⽅法的功能称为 Java 语⾔的反射机制。
⽐如,获取⼀个 Class 对象。 Class.forName( 完整类名 ) 。通过 Class 对象获取类的构造⽅法,
class.getConstructor 。根据 Class 对象获取类的⽅法, getMethod 和 getMethods 。使⽤
Class 对象创建⼀个对象, class.newInstance 等。反射具有以下特性:
- 运⾏时类信息访问:反射机制允许程序在运⾏时获取类的完整结构信息,包括类名、包名、⽗
类、实现的接⼝、构造函数、⽅法和字段等。
- 动态对象创建:可以使 ⽤反射API 动态地创建对象实例,即使在编译时不知道具体的类名。这是
通过Class 类的newInstance() ⽅法或Constructor 对象的newInstance() ⽅法实现的。- 动态⽅法调⽤:可以在运⾏时动态地调⽤对象的⽅法,包括私有⽅法。这通过Method 类的
invoke() ⽅法实现,允许你传 ⼊对象实例和参数值来执⾏⽅法。- 访问和修改字段值:反射还允许程序在运⾏时访问和修改对象的字段值,即使是私有的。这是

通过Field 类的get() 和set() ⽅法完成的。反射的优点就是增加灵活性,可以在运⾏时动态获取对象实例。缺点是反射的效率很低,⽽且会
破坏封装,通过反射可以访问类的私有⽅法,不安全。
类加载的具体过程,双亲委派机制
我们把 Java 的类加载过 程分为三个主 要步骤:加载、链接、初始化。
⾸先是加载阶段(Loading ),它是 Java 将字节码数据从不 同的数据源读取到 JVM 中,并映射为
JVM 认可的数据结构(Class 对象),这⾥的数据源可能是各种各样的形态,如 jar ⽂件、class ⽂件,甚⾄是⽹络数据源等;如果输⼊数据不是 ClassFile 的结构,则会抛出 ClassFormatError 。
加载阶段是⽤⼾参与的阶段,我们可以⾃定义类加载器, 去实现⾃⼰的类加载过 程。
第⼆阶段是链接(Linking ),这是核⼼的步骤,简单说是把原始的类定义信息平滑地转化⼊ JVM
运⾏的过程中。这⾥可进⼀步细分为三个 步骤:
验证(Verification ),这是虚拟机安全的重要保障,JVM 需要核验字节信息是符合 Java 虚拟机
规范的,否则就被认为是 VerifyError ,这样就防⽌了恶意信息或者不合规的信息危害 JVM 的运
⾏,验证阶段有可能触发更多 class 的加载。准备(Preparation ),创建类或接⼝中的静态变量,并初始化静态变量的初始值。但这⾥的“初
始化”和下⾯的显式初始化阶段是有区别的,侧重点在于分配所需要的内存空间,不会去执⾏更
进⼀步的 JVM 指令。
解析(Resolution ),在这⼀步会将常量池中的符号引⽤(symbolic reference )替换为直接引
⽤。
最后是初始化阶段(initialization ),这⼀步真正去执⾏类初始化的代码逻辑,包括静态字段赋值的动作,以及执⾏类定义中 的静态初始化块内的逻辑,编译器在编译阶段就会把这部分逻辑整理
好,⽗类型的初始化逻辑优先于当前类型的逻辑。
再来谈谈 双亲委派模型,简单说就是当类加载器( Class-Loader )试图加载某个类型的时候,除⾮⽗加载器找不到相应类型,否则尽量将这个任务代理给当前加 载器的⽗加载器去做。使⽤委派模
型的⽬的是避免重复加载 Java 类型。
TCP 四次挥⼿的过程?

具体过程:
客⼾端主动调⽤关闭连接的函数,于是就会发送 FIN 报⽂,这个 FIN 报⽂代表客⼾端不会再发
送数据了,进⼊ FIN_WAIT_1 状态;服务端收到了 FIN 报⽂,然后⻢上回复⼀个 ACK 确认报⽂,此时服务端进⼊ CLOSE_WAIT 状
态。在收到 FIN 报⽂的时候,TCP 协议栈会为 FIN 包插⼊⼀个⽂件结束符 EOF 到接收缓冲区
中,服务端应⽤程序可以通过 read 调⽤来感知这个 FIN 包,这个 EOF 会被放在已排队等候的
其他已接收的数据之后,所以必须要得继续 read 接收缓冲区已接收的数据;
接着,当服务端在 read 数据的时候,最后⾃然就会读到 EOF ,接着 read() 就会返回 0,这时服务端应⽤程序如果有 数据要发送的话,就发完数据后才调⽤关闭连接的函数,如果服 务端应⽤
程序没有数据要发送的话,可以直接调⽤关闭连接的函数,这时服务端就会发⼀个 FIN 包,这
个 FIN 报⽂代表服务端不会再发送数据了,之后处于 LAST_ACK 状态;
客⼾端接收到服务端的 FIN 包,并发送 ACK 确认包给服务端,此时客⼾端将进⼊ TIME_WAIT
状态;
服务端收到 ACK 确认包后,就进⼊了最后的 CLOSE 状态;
客⼾端经过 2MSL 时间之后,也进⼊ CLOSE 状态;
多线程的优势,为什么 要有多线程?
使⽤多线程的理由之⼀是和进程相⽐,它是⼀种⾮常花销⼩,切换快,更"节俭"的多任务操作⽅
式。在Linux 系统下,启动⼀个新的进程必须分配给它独⽴的地址 空间,建⽴众多的数据表来维护
它的代码段、堆栈段和数据段,这是⼀种"昂贵"的多任务⼯作⽅式。⽽运⾏于⼀个进程中的多个线
程,它们彼此之间使⽤相同的地址 空间,共享⼤部分数据,启动⼀个线程所花费的空间远远 ⼩于
启动⼀个进程所花费的空间,⽽且,线程间彼此切换所需的时间也远远 ⼩于进程间切换所需要的
时间。
使⽤多线程的理由之⼆是线程间⽅便的通信机制。对不同进程来说,它们具有独⽴的数据空间,
要进⾏数据的传递只能通过通信的⽅式进⾏,这种⽅式不仅 费时,⽽且很不⽅便。线程则不然,
由于同⼀进程下的线程之间共享数据空间,所以⼀个线程的数据可以直接为其它线程所⽤,这不仅快捷,⽽且⽅便。当然,数据的共享也 带来其他⼀些问题,有的变量不能同时被两个 线程所修
改,有的⼦程序中声明为static 的数据更有 可能给多线程程 序带 来灾难性的打击,这些正是编写多
线程程 序时最需要注意的地⽅。
除了以上所说的优点外,不和进程⽐较,多线程程 序作为⼀种多任务、并发的⼯作⽅式,当然有
以下的优点:
提⾼应⽤程序响应。这对图形界⾯的程序尤其有意义,当⼀个操作耗时很⻓时,整个系统都会
等待这个操作,此时程序不会响应键盘、⿏标、菜单的操作,⽽使⽤多线程技术,将耗时⻓的
操作置于⼀个新的线程,可以避免这种尴尬 的情况。
使多CPU 系统更加有效。操作系统会保 证当线程数不⼤于CPU 数⽬时,不同的线程运⾏于不 同的
CPU 上。
改善程序结构。⼀个既⻓⼜复杂的进程可以考虑分为多个线程,成为⼏个独⽴或半独⽴的运⾏
部分,这样的程序会利于理解和修改。
死锁的具体原因,怎么解决?
死锁只有同时满⾜以下四个条件才会发⽣:
互斥条件:互斥条件是指多个线程不能同时使⽤同⼀个资源。
持有并等待条件:持有并等待条件是指,当线程 A 已经持有了资源 1,⼜想申请资源 2,⽽资
源 2 已经被线程 C 持有了,所以线程 A 就会处于等待状态,但是线程 A 在等待资源 2 的同时
并不会释放⾃⼰已经持有的资源 1。
不可剥夺条件:不可剥夺条件是指,当线程已经持有了资源 ,在⾃⼰使⽤完之前不能被其他线
程获取,线程 B 如果也想使⽤此资源,则只能在线程 A 使⽤完并释放后才能获取。
环路等待条件:环路等待条件指的是,在死锁发⽣的时候,两个 线程获取资源的顺序构成了环
形链。
避免死锁问题就只需要破环其中⼀个条件就可以,最常⻅的并且可⾏的就是使⽤资源有序分配
法,来破环环 路等待条件。
那什么 是资源有序分配法呢?线程 A 和 线程 B 获取资源的顺序要⼀样,当线程 A 是先尝试获取资
源 A,然后尝试获取资源 B 的时候,线程 B 同样也是先尝试获取资源 A,然后尝试获取资源 B。也
就是说,线程 A 和 线程 B 总是以相同的顺序申请⾃⼰想要的资源。

Java 并发的⼯作原理?
Java 并发编程是指在Java 程序中同时执⾏多个线程,以提⾼程序的运⾏效率和响 应能⼒,Java 中的
Thread 类和 Runnable 接⼝是实现多线程的基础。每个线程都有⾃⼰独⽴的执⾏路径,通过调⽤
Thread 类的 start() ⽅法启动线程。Java 线程的⽣命周 期包括:
新建状态(New ):线程被创建,但尚未开始。
就绪状态(Runnable ):线程准备好 并等待CPU 分配时间⽚。
运⾏状态(Running ):线程正在执⾏。
阻塞状态(Blocked ):线程因某种原因(如等待锁)⽽被阻塞。
死亡状态(Terminated ):线程已执⾏完毕。
要保证多线程的允许是安全,不要出现数据竞争造成的数据混乱的问题。Java 的线程安全在三个 ⽅
⾯体现:
原⼦性:提供互斥访问,同⼀时刻只能有⼀个线程对数据进⾏操作,在Java 中使⽤了atomic 和
synchronized 这两个 关键字来确保原⼦性;
可⻅性:⼀个线程对主内存的修改可以及时地被其他线程看到,在Java 中使⽤了synchronized 和
volatile 这两个 关键字确保可⻅性;
有序性:⼀个线程观察其他线程中的指令执⾏顺序,由于指令重排序,该观察结果⼀般杂乱⽆
序,在Java 中使⽤了happens-before 原则来确保有序性。
Java 提供了多种锁机制,可以分为以下⼏类:
内置锁(synchronized ):Java 中的 synchronized 关键字是内置锁机制的基础,可以⽤于⽅法
或代码块。当⼀个线程进⼊ synchronized 代码块或⽅法时,它会获取关联对象的锁;当线程离
开该代码块或⽅法时,锁会被释放。如果其他线程尝试获取同⼀个对象的锁,它们将被阻塞,
直到锁被释放。其中,syncronized 加锁时有⽆锁、偏向锁、轻量级锁和重量 级锁⼏个级别。偏
向锁⽤于当⼀个线程进⼊同步块时,如果没有任何 其他线程竞 争,就会使 ⽤偏向锁,以减少锁
的开销。轻量级锁使⽤线程栈上的数据结构,避免了操作系统级 别的锁。重量 级锁则涉及操作
系统级 的互斥锁。
ReentrantLock: java.util.concurrent.locks.ReentrantLock 是⼀个显式的锁类,提供了⽐
synchronized
更⾼级的功能,如可中断的锁等待、定时锁等待、公平锁选项等。
ReentrantLock
使⽤ lock() 和 unlock() ⽅法来获取和释放锁。其中,公平锁按照线程请求锁的顺序来分配锁,保证了锁分配的公平性,但可能增加锁的等待时间。⾮公平锁不保证锁分配
的顺序,可以减少锁的竞争,提⾼性能,但可能造成某些线程的饥饿 。
读写锁(ReadWriteLock ): java.util.concurrent.locks.ReadWriteLock 接⼝定义了 ⼀种锁,
允许多个读取者同时访问共享资源,但只允许⼀个写⼊者。读写锁通常⽤于读取远多于写⼊的
情况,以提⾼并发性。
乐观锁和悲观锁:悲观锁(Pessimistic Locking )通常指在访问数据前就锁定资源,假设最坏的
情况,即数据很可能被其他线程修改。 synchronized 和 ReentrantLock 都是悲观锁的例⼦。乐
观锁(Optimistic Locking )通常不锁定资源,⽽是在更新数 据时检查数据是否已被其他线程修
改。乐观锁常使⽤版本号或时间戳来实现。
⾃旋锁:⾃旋锁是⼀种锁机制,线程在等待锁时会持续循环检查锁是否可 ⽤,⽽不是放弃CPU
并阻塞。通常可以使 ⽤CAS 来实现。这在锁等待时间很短的情况下可以提⾼性能,但过度⾃旋
会浪费CPU 资源。
常⽤的git 操作有哪些?

⼏个专 ⽤名词的译名如下:
Workspace :⼯作区
Index / Stage :暂存区
Repository :仓库区(或本地仓库)
Remote :远程仓库
⼯作流程
最基础的⼯作流程,⾸先执⾏ git pull 获取远程仓库的最新代码,进⾏代码的编写。
完成相应功能的开发后执⾏ git add . 将⼯作区代码的修改添加到 暂存区,再执⾏ git commit
-m 完成 xx 功能 将暂存区代码提交到本地仓库并 添加相应的注释,最后执⾏ git push 命令推送到
远程仓库。
撤回 git commit 操作
当执⾏了 git commit -m 注释内容 命令想要撤回,可以使 ⽤ git reset --soft HEAD^ 把本地仓库回退到当前版本的上⼀个版本,也就是刚刚 还没提交的时候,代码的改动会保 留在暂存区和⼯
作区。
也可以使 ⽤ git reset --mixed HEAD^ ,这样不⽌回退了刚刚 的 git commit 操作,还回退了git add 操作,代码的改动只会保 留在⼯作区。因为 --mixed 参数是 git reset 命令的默认
选项,也就是可以写为 git reset HEAD^ 。撤回 git push 操作
当执⾏了 git push 命令想要撤回,可以使 ⽤ git reset HEAD^ 将本地仓库回退到当前版本的上⼀个版本,代码的修改会保 留在⼯作区,然后使⽤ git push origin xxx --force 将本地仓库当
前版本的代码强制推送到远程仓库。
算法题
leetcode 3. ⽆重复字符的最⻓⼦串(中等)
leetcode 88. 合并两个 有序数组(简单)
