
面试刷题网站:
大家好,我是小林。
互联网大厂的秋招启动得早,面试效率也高。一般来说,3 轮技术面加 1 轮 HR 面,整个流程大概 2-3 周就能完成。
所以最近能拿到秋招 offer 的同学,基本都是互联网中大厂的 offer。
而中小公司的面试时间会相对靠后,因为这类公司的主要招聘场景,很多是在高校举办的双选会上 , 通常当天面试完就能当场发 offer。
10 到 11 月份是双选会的高峰期,同学们可以抓住这波机会,在线下集中收割 offer。
这不,我们就有位同学传来了喜讯,秋招终于告一段落了,成功熬出头了!他面完蚂蚁二面后,一度以为自己凉了,没想到一周后收到了 HR 面试通知,并最终顺利通关,拿下了蚂蚁的 Offer,激动得不行。

拿到了录用通知,剩下就坐等统一薪资开奖了。
说到这,顺便给大家同步一下蚂蚁 25 届的薪资情况,供 26 届的同学参考。后端开发基本能拿到 35W+ 的年薪,还是相当香的。
虽然拿到一个大厂 offer 已经很棒了,但我真心建议大家:先别急着躺平!
手里的 offer 越多,你谈薪的底气就越足。
举个例子:你只有一个 offer,想跟 HR 要 28k 可能有点虚。但如果你手上有好几个 offer,其中一个已经开到 26k,你再去要 28k 的期望薪资,成功率会大得多。手里的 offer 就是你最好的筹码。
目前蚂蚁的秋招还在进行中,我们正好来看看一份新鲜出炉的蚂蚁 Java 一面面经。
蚂蚁和阿里的风格很像,喜欢深挖 Java 八股文,尤其是 Java 集合和并发这两块,面试官会往原理问,其次就是 MySQL 和网络了,当然最后还是手撕算法,大厂终究还是要考察算法的。

蚂蚁一面1. HashMap如何解决hash冲突?
在 JDK 1.7 版本之前, HashMap 数据结构是数组和链表,HashMap通过哈希算法将元素的键(Key)映射到数组中的槽位(Bucket)。如果多个键映射到同一个槽位,它们会以链表的形式存储在同一个槽位上,因为链表的查询时间是O(n),所以冲突很严重,一个索引上的链表非常长,效率就很低了。

所以在 JDK 1.8版本的时候做了优化,当一个链表的长度超过8的时候就转换数据结构,不再使用链表存储,而是使用红黑树,查找时使用红黑树,时间复杂度O(log n),可以提高查询性能,但是在数量较少时,即数量小于6时,会将红黑树转换回链表。

2. 用HashMap举例说明什么是线程安全?
HashMap是 Java 中常用的哈希表实现,但它不是线程安全的。这意味着当多个线程同时对同一个HashMap实例进行修改操作(如添加、删除元素)时,可能会导致数据不一致、死循环甚至程序崩溃等问题。
下面是一个演示HashMap线程不安全的例子:
importjava.util.HashMap;
importjava.util.Map;
importjava.util.concurrent.ExecutorService;
importjava.util.concurrent.Executors;
publicclassHashMapThreadSafetyExample{
publicstaticvoidmain(String[] args){
// 创建一个非线程安全的HashMap
Map
// 创建线程池
ExecutorService executor = Executors.newFixedThreadPool(5);
// 多个线程同时向HashMap中添加元素
for(inti = 0; i < 1000; i++) {
finalintkey = i;
executor.execute( -> {
map.put(key, "value"+ key);
});
}
// 关闭线程池并等待所有任务完成
executor.shutdown;
while(!executor.isTerminated) {
// 等待所有任务完成
}
// 理论上map的大小应该是1000,但实际可能小于1000
System.out.println("HashMap的大小: "+ map.size);
}
}
在这个例子中,我们创建了一个HashMap,并使用 5 个线程同时向其中添加 1000 个元素。由于HashMap不是线程安全的,多个线程同时修改它时,可能会出现以下问题:
数据丢失:某些元素可能没有被正确添加到 map 中,导致最终 map 的大小小于 1000。
死循环:在极端情况下,可能会导致链表形成环形结构,从而在 get 操作时陷入死循环。
数据不一致:获取元素时可能得到错误的结果。
相比之下,ConcurrentHashMap是线程安全的哈希表实现。它通过内部的分段锁机制,允许多个线程同时访问和修改不同的段,从而在保证线程安全的同时提供较好的性能。
如果我们将上面例子中的HashMap替换为ConcurrentHashMap,程序的输出将始终是 1000,因为它保证了多线程环境下的数据一致性。
所以,线程安全就是指在多线程环境下,某个数据结构或方法能够保证其操作的原子性、可见性和有序性,从而避免出现数据不一致等问题。HashMap不是线程安全的,而ConcurrentHashMap是线程安全的实现。
3. Java如何解决HashMap的线程安全问题?
方式一:使用 Collections.synchronizedMap包装。
java.util.Collections提供了 synchronizedMap方法,能将普通 HashMap包装为线程安全的集合。其原理是对 HashMap的所有方法添加同步锁(使用 synchronized关键字),确保同一时刻只有一个线程能操作集合。代码如下:
importjava.util.Collections;
importjava.util.HashMap;
importjava.util.Map;
publicclassSynchronizedMapExample{
publicstaticvoidmain(String[] args){
// 创建普通HashMap,并用synchronizedMap包装
Map
// 多线程环境下使用syncMap
// ...
}
}
特点是实现简单,直接包装即可。性能较差,因为所有方法都用同一把锁,多线程操作时会频繁阻塞,适合并发量低的场景。
方式二:使用 ConcurrentHashMap(推荐)
ConcurrentHashMap是 Java 并发包(java.util.concurrent)提供的线程安全哈希表,专为高并发场景设计。
JDK 1.7 及之前:采用「分段锁」机制,将哈希表分为多个段(Segment),每个段独立加锁。多线程访问不同段时不会冲突,效率较高。
JDK 1.8 及之后:优化为「CAS + synchronized」机制,移除了分段锁,直接对哈希表的节点(Node)加锁,进一步提升并发性能。
importjava.util.concurrent.ConcurrentHashMap;
publicclassConcurrentHashMapExample{
publicstaticvoidmain(String[] args){
// 创建线程安全的ConcurrentHashMap
Map
// 多线程环境下安全操作
concurrentMap.put(1, "a");
String value = concurrentMap.get(1);
}
}
特点是性能优异,支持多线程同时读写,锁粒度更细(JDK 1.8 中为节点级锁),推荐用于高并发场景(如服务器端程序)。
方式三:手动加锁(不推荐)
在操作 HashMap时,手动使用 synchronized或 ReentrantLock加锁,确保同一时刻只有一个线程修改集合。
importjava.util.HashMap;
importjava.util.Map;
importjava.util.concurrent.locks.Lock;
importjava.util.concurrent.locks.ReentrantLock;
publicclassManualLockExample{
privatefinalMap
privatefinalLock lock = newReentrantLock; // 可重入锁
publicvoidput(Integer key, String value){
lock.lock; // 加锁
try{
map.put(key, value);
} finally{
lock.unlock; // 释放锁,确保异常时也能释放
}
}
publicString get(Integer key){
lock.lock;
try{
returnmap.get(key);
} finally{
lock.unlock;
}
}
}
灵活性高,但需手动控制锁的获取和释放,容易因遗漏 unlock导致死锁。性能与 synchronizedMap类似,不如 ConcurrentHashMap。
4. CAS是什么?
CAS 是一种乐观锁机制,用于在多线程环境下实现无锁并发控制,确保数据操作的原子性。
CAS 操作包含三个核心参数:
内存地址(V):要操作的变量在内存中的地址。
预期值(A):线程认为操作前,变量的预期值。
新值(B):如果预期值与内存中的实际值一致,就将变量更新为新值。
执行时,线程先读取内存地址 V 中的值并记录为预期值 A,接着计算要更新的新值 B,然后再次读取 V 中的实际值并与预期值 A 比较,如果相等,说明值未被其他线程修改,就将 V 更新为 B,操作成功;如果不等,说明值已被其他线程修改,就放弃更新,操作失败,通常会重试或放弃。整个过程是由 CPU 指令直接支持的原子操作,无需使用 synchronized 等重量级锁。
5. 线程池的好处是?
线程在正常执行或者异常中断时会被销毁,如果频繁的创建很多线程,不仅会消耗系统资源,还会降低系统的稳定性,一不小心把系统搞崩了。
使用线程池可以带来以下几个好处:
线程池可以降低资源消耗。线程的创建和销毁需要消耗系统资源(如 CPU 时间、内存),线程池通过复用已创建的线程,避免了频繁创建和销毁线程带来的开销,尤其在需要频繁使用线程的场景下效果显著。
线程池能提高响应速度。当任务到达时,无需等待线程创建就能立即执行(如果池中有空闲线程),减少了任务的等待时间,提升了系统的即时响应能力。
线程池便于管理线程。通过线程池可以统一控制线程的数量、优先级等,避免因无限制创建线程导致系统资源耗尽(如内存溢出、CPU 过度占用),还能对线程进行监控、调优和扩展,使线程管理更加规范和灵活。
所以,线程池是为了减少频繁的创建线程和销毁线程带来的性能损耗。
6. 创建线程池时需要考虑哪些参数?
ThreadPoolExecutor的构造函数:
publicThreadPoolExecutor(
intcorePoolSize,
intmaximumPoolSize,
longkeepAliveTime,
TimeUnit unit,
BlockingQueue
ThreadFactory threadFactory,
RejectedExecutionHandler handler
)
核心线程数(corePoolSize):线程池长期维持的最小线程数量,即使线程空闲也不会被销毁,对于 CPU 密集型任务,建议设置为 CPU 核心数附近;对于 I/O 密集型任务,可以设置得更大一些。
最大线程数(maximumPoolSize):线程池允许创建的最大线程数量,当核心线程忙碌且任务队列满时会创建新线程直到该值,需考虑系统能承受的最大并发量和任务峰值压力
空闲线程存活时间(keepAliveTime):超过核心线程数的 “临时线程” 空闲时的存活时间,超时后会被销毁,需结合任务波动频率设置
时间单位(unit):配合 keepAliveTime 使用,指定时间单位(如毫秒、秒等)
任务队列(workQueue):用于缓存等待执行任务的阻塞队列,需根据任务大小和数量选择,如无界队列、有界队列或优先级队列
线程工厂(threadFactory):用于创建线程的工厂,可自定义线程名称、优先级等,需考虑线程可追溯性和优先级设置
拒绝策略(handler):当任务队列满且线程数达最大值时,对新提交任务的处理策略,需根据业务对任务丢失的容忍度选择,如抛出异常、丢弃任务等
线程池的工作原理如下图:

当用户提交了一个任务,接下来这个任务将如何执行都是由这个阶段决定的。首先,所有任务的调度都是由execute方法完成的,这部分完成的工作是:检查现在线程池的运行状态、运行线程数、运行策略,决定接下来执行的流程,是直接申请线程执行,或是缓冲到队列中执行,亦或是直接拒绝该任务。其执行过程如下:
首先检测线程池运行状态,如果不是RUNNING,则直接拒绝,线程池要保证在RUNNING的状态下执行任务。
如果workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务。
如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。
如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务。
如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常。
B + 树是一种多路平衡查找树:

树结构特征:B + 树由根节点、中间节点和叶子节点组成,所有节点按层次有序排列。每个非叶子节点(根、中间节点)只存储索引键和子节点指针,不存储实际数据;叶子节点则按索引键顺序串联成一个双向链表,既存储索引键,也存储对应数据的物理地址(或直接存储数据记录)。
多路平衡特性:每个节点可以包含多个索引键,通过多个分支减少树的高度(通常 3-4 层即可支持千万级数据)。同时,树始终保持平衡状态,即从根到任意叶子节点的路径长度相同,避免了二叉树可能出现的极端倾斜问题。
查询路径固定:所有查询最终都会落到叶子节点,因为非叶子节点仅作为索引导航,不存储实际数据。叶子节点的双向链表结构支持范围查询(如between、in等),只需找到范围的起始和结束位置,即可通过链表快速遍历中间所有记录。
B + 树通过优化结构减少了磁盘 I/O、利用了磁盘存储特性,并针对性优化了范围查询,从而显著提升了数据库的查询效率,因此MySQL 默认的存储引擎 InnoDB 采用的是 B+ 作为索引的数据结构。
9. HTTPS实现安全通信的原理
传统的 TLS 握手基本都是使用 RSA 算法来实现密钥交换的,在将 TLS 证书部署服务端时,证书文件其实就是服务端的公钥,会在 TLS 握手阶段传递给客户端,而服务端的私钥则一直留在服务端,一定要确保私钥不能被窃取。
在 RSA 密钥协商算法中,客户端会生成随机密钥,并使用服务端的公钥加密后再传给服务端。根据非对称加密算法,公钥加密的消息仅能通过私钥解密,这样服务端解密后,双方就得到了相同的密钥,再用它加密应用消息。
我用 Wireshark 工具抓了用 RSA 密钥交换的 TLS 握手过程,你可以从下面看到,一共经历了四次握手:


TLS 第一次握手
首先,由客户端向服务器发起加密通信请求,也就是 ClientHello 请求。在这一步,客户端主要向服务器发送以下信息:
(1)客户端支持的 TLS 协议版本,如 TLS 1.2 版本。
(2)客户端生产的随机数(Client Random),后面用于生成「会话秘钥」条件之一。
(3)客户端支持的密码套件列表,如 RSA 加密算法。
TLS 第二次握手
服务器收到客户端请求后,向客户端发出响应,也就是 SeverHello。服务器回应的内容有如下内容:
(1)确认 TLS 协议版本,如果浏览器不支持,则关闭加密通信。
(2)服务器生产的随机数(Server Random),也是后面用于生产「会话秘钥」条件之一。
(3)确认的密码套件列表,如 RSA 加密算法。(4)服务器的数字证书。
TLS 第三次握手
客户端收到服务器的回应之后,首先通过浏览器或者操作系统中的 CA 公钥,确认服务器的数字证书的真实性。
如果证书没有问题,客户端会从数字证书中取出服务器的公钥,然后使用它加密报文,向服务器发送如下信息:
(1)一个随机数(pre-master key)。该随机数会被服务器公钥加密。
(2)加密通信算法改变通知,表示随后的信息都将用「会话秘钥」加密通信。
(3)客户端握手结束通知,表示客户端的握手阶段已经结束。这一项同时把之前所有内容的发生的数据做个摘要,用来供服务端校验。
上面第一项的随机数是整个握手阶段的第三个随机数,会发给服务端,所以这个随机数客户端和服务端都是一样的。
服务器和客户端有了这三个随机数(Client Random、Server Random、pre-master key),接着就用双方协商的加密算法,各自生成本次通信的「会话秘钥」。
TLS 第四次握手
服务器收到客户端的第三个随机数(pre-master key)之后,通过协商的加密算法,计算出本次通信的「会话秘钥」。
然后,向客户端发送最后的信息:
(1)加密通信算法改变通知,表示随后的信息都将用「会话秘钥」加密通信。
(2)服务器握手结束通知,表示服务器的握手阶段已经结束。这一项同时把之前所有内容的发生的数据做个摘要,用来供客户端校验。
至此,整个 TLS 的握手阶段全部结束。接下来,客户端与服务器进入加密通信,就完全是使用普通的 HTTP 协议,只不过用「会话秘钥」加密内容。
10. 非对称加密原理是什么?
非对称加密有两个密钥,分别是公钥和私钥。可以用公钥加密数据,然后要用私钥解密,也可以用私钥加密,然后用公钥加密数据。
具体来说,非对称加密的工作流程是:发送方使用接收方公开的公钥对数据进行加密,加密后的密文只能通过接收方独有的私钥才能解密;反之,若发送方用自己的私钥对数据(通常是数据的哈希值)加密,接收方则可使用对应的公钥验证签名,确认数据未被篡改且发送者身份真实。
这种 “公钥加密、私钥解密”“私钥签名、公钥验签” 的机制,解决了对称加密中密钥传输的安全问题。
11. 对称加密与非对称加密的区别是什么?
密钥机制:对称加密使用单一密钥(密钥 A)进行加密和解密,加密和解密过程使用同一密钥,且密钥需要在通信双方之间提前共享。非对称加密使用一对密钥(公钥和私钥),公钥可公开传播,私钥由持有者保密;用公钥加密的数据只能用对应私钥解密,用私钥加密的数据(通常用于签名)只能用对应公钥验证,无需共享私钥。
安全性基础:对称加密的安全性依赖于密钥的保密性,一旦密钥泄露,加密数据会被破解;其加密算法(如 AES)本身数学复杂度较低,主要通过密钥长度(如 128 位、256 位)保证安全性。非对称加密的安全性基于数学难题(如 RSA 依赖大整数分解,ECC 依赖椭圆曲线离散对数问题),即使公钥公开,也难以从公钥推导出私钥,安全性更高,但算法本身计算复杂。
性能与适用场景:对称加密算法(如 AES、DES)计算简单、速度快,适合大量数据加密(如文件、流数据传输),但密钥传输过程存在泄露风险。非对称加密(如 RSA、ECC)计算量大、速度慢,不适合大文件加密,主要用于密钥交换(如用公钥加密对称密钥)、数字签名(验证身份和数据完整性)等场景。
数组中三个连续子数组的最大和。