一、基础概念篇(1-15)
第1题:进程和线程的区别?
核心回答:
· 进程是操作系统资源分配的最小单位,拥有独立的内存空间(堆、栈、数据段、代码段),进程间相互隔离。
· 线程是CPU调度的最小单位,是进程内的一个执行路径。同一进程的多个线程共享进程的堆和方法区(JDK1.8后叫元空间),但每个线程有自己的程序计数器、虚拟机栈和本地方法栈。
底层原理:
· 进程切换需要切换页表、刷新TLB(快表),开销大;线程切换只需切换栈和寄存器,开销小。
· Linux中进程和线程都用task_struct描述,只是共享资源程度不同。
开发经验:
· 一个进程默认一个主线程,多线程能充分利用多核CPU。
· 线程间通信方便(共享内存),但要考虑线程安全;进程间通信(IPC)如管道、Socket、共享内存更复杂。
· 实际开发中:Tomcat每个请求用一个线程,而不是一个进程,因为线程创建/销毁开销远小于进程。
第2题:并行和并发有什么区别?
核心回答:
· 并发(Concurrency):同一时间段内,多个任务交替执行。在单核CPU上,通过时间片轮转实现,宏观上看起来"同时",微观上串行。
· 并行(Parallelism):同一时刻,多个任务同时执行。必须有多核CPU或多台机器。
经典比喻:
· 并发 = 一个人吃三个馒头,轮着咬
· 并行 = 三个人各吃一个馒头
开发经验:
· 我们写的Java多线程代码,默认是并发的(由操作系统调度)。
· 要真正并行,需要利用ForkJoinPool、parallelStream()或CompletableFuture自定义线程池。
· 面试陷阱:Runtime.getRuntime().availableProcessors()获取CPU核数,并行度设置不当会适得其反。
第3题:创建线程的三种方式?
核心回答:
方式一:继承Thread类
java
class MyThread extends Thread {
@Override
public void run() { System.out.println("hello"); }
}
new MyThread().start();
方式二:实现Runnable接口
java
class MyTask implements Runnable {
@Override
public void run() { System.out.println("hello"); }
}
new Thread(new MyTask()).start();
方式三:实现Callable接口(有返回值)
java
class MyCallable implements Callable<String> {
@Override
public String call() throws Exception { return "hello"; }
}
FutureTask<String> ft = new FutureTask<>(new MyCallable());
new Thread(ft).start();
String result = ft.get(); // 阻塞获取结果
底层原理:
· Thread.start()会调用native start0(),由JVM创建操作系统线程并执行run()。
· Runnable是函数式接口,@FunctionalInterface,可用Lambda简化。
开发经验:
· 实际生产禁止直接new Thread(),必须用线程池。原因:线程创建销毁开销大,且无限制创建会导致OOM。
· Callable配合FutureTask可获取结果和异常,比Runnable更灵活。
第4题:run()和start()区别?
核心回答:
· start():启动一个新线程,JVM会调用该线程的run()方法。只能调用一次,再次调用抛IllegalThreadStateException。
· run():普通实例方法,直接调用不会创建新线程,会在当前线程同步执行。
示例:
java
Thread t = new Thread(() -> System.out.println(Thread.currentThread().getName()));
t.run(); // 输出main,没有新线程
t.start(); // 输出Thread-0,新线程
底层原理:
· start()是synchronized方法,会检查threadStatus是否为0(NEW状态),然后加入线程组,最后调用start0()。
开发经验:
· 面试高频,一定要能说清。实际中不会直接调用run(),除非故意想在主线程执行。
第5题:线程生命周期状态?
核心回答:
Java线程有6种状态(Thread.State枚举):
状态 说明
NEW 创建但未start
RUNNABLE 可运行状态,包含就绪和运行中
BLOCKED 等待获取锁(synchronized)
WAITING 无限期等待,需notify/notifyAll唤醒
TIMED_WAITING 有限期等待,如sleep(1000)
TERMINATED 已结束
状态流转:
NEW → RUNNABLE → BLOCKED/WAITING/TIMED_WAITING → RUNNABLE → TERMINATED
底层原理:
· JVM状态与OS状态映射:RUNNABLE对应OS的Ready和Running。
· BLOCKED vs WAITING:BLOCKED是被动等待锁释放,WAITING是主动调用wait/park,需被唤醒。
开发经验:
· jstack查看线程状态,排查死锁看BLOCKED,排查不响应看WAITING。
· sleep()不释放锁,wait()释放锁,这是关键区别。
第6题:什么是线程优先级?
核心回答:
· Java线程优先级范围1~10,默认5(Thread.NORM_PRIORITY)。
· 优先级高的线程获取CPU时间片的概率更大,但不保证。
底层原理:
· 映射到操作系统原生优先级(Windows有7级,Linux无优先级映射,全部为普通线程)。
· 依赖于OS调度器,不同OS实现不同。
开发经验:
· 不要依赖优先级做业务逻辑,这是不可移植的。
· 优先级可能导致优先级反转(低优先级持有锁,高优先级等待),不过JVM会处理。
· 实际开发中很少设置优先级,默认即可。
第7题:守护线程是什么?
核心回答:
· 守护线程(Daemon Thread)是后台线程,当所有非守护线程结束时,JVM会自动退出,守护线程立即终止(不保证finally执行)。
· 典型:GC线程、JIT编译线程。
使用:
java
Thread t = new Thread(() -> {});
t.setDaemon(true); // 必须在start()之前设置
t.start();
底层原理:
· JVM退出时只检查非守护线程是否存活。
· 守护线程中创建的子线程默认也是守护线程。
开发经验:
· 绝对不能在守护线程中执行finally块释放资源,因为JVM不会等待守护线程的finally执行完。
· 日志刷盘、数据库连接等操作不要用守护线程。
· 适合后台监控、心跳检测等非核心任务。
第8题:什么是上下文切换?
核心回答:
· CPU从一个线程/进程切换到另一个,需要保存当前线程的上下文(寄存器、程序计数器、栈信息),加载下一个线程的上下文。
· 上下文切换有开销(CPU时间+缓存失效),频繁切换影响性能。
底层原理:
· 由操作系统调度器触发:时间片用完、IO等待、锁竞争、中断等。
· 切换时会触发TLB刷新,导致缓存命中率下降。
开发经验:
· 减少上下文切换的手段:
· 减少锁竞争(锁粗化、减小锁粒度)
· 减少阻塞(异步IO替代同步IO)
· 合理设置线程池大小(线程太多反而切换频繁)
· 用vmstat或perf可监控上下文切换次数。
第9题:什么是线程安全?
核心回答:
· 当多个线程访问共享数据时,无论线程如何交替执行,结果都与单线程执行一致,则称线程安全。
· 线程不安全本质:原子性、可见性、有序性被破坏。
三个核心概念:
· 原子性:操作不可分割,如i++不是原子(读-改-写三步)。
· 可见性:一个线程修改,其他线程立即看到。CPU缓存导致可见性问题。
· 有序性:指令重排导致结果不符合预期。
开发经验:
· 无状态对象(没有成员变量)天然线程安全。
· 有状态对象要加锁或使用并发容器。
· 实际项目:大部分Service、Controller都是无状态的,Spring默认单例没问题。
第10题:synchronized和volatile区别?
核心回答:
特性 synchronized volatile
原子性 ✅ 保证 ❌ 不保证
可见性 ✅ 保证 ✅ 保证
有序性 ✅ 保证 ✅ 保证(禁止重排)
锁 重量级/轻量级 无锁
适用 复合操作 简单赋值/状态标记
典型用法:
java
// volatile适合状态标记
volatile boolean running = true;
// synchronized适合复合操作
synchronized(this) { count++; }
底层原理:
· volatile底层通过内存屏障(lock前缀指令)实现,强制刷新到主内存。
· synchronized通过monitor enter/exit实现,底层是JVM的锁升级机制。
开发经验:
· 计数器用AtomicInteger(CAS),不要用volatile int count++。
· DCL单例中volatile是必须的,防止指令重排。
第11题:什么是CAS?
核心回答:
· CAS(Compare And Swap)是乐观锁实现,三个参数:内存地址V、期望值A、新值B。
· 如果V的值等于A,则原子更新为B,否则失败重试(自旋)。
底层原理:
· 由CPU指令支持(如x86的cmpxchg),是硬件级别的原子操作。
· Java中Unsafe.compareAndSwapInt()是native方法。
ABA问题:
· V从A→B→A,CAS会认为没变化。用AtomicStampedReference带版本号解决。
开发经验:
· AtomicInteger、AtomicLong、ConcurrentHashMap都基于CAS。
· 缺点:自旋长时间占用CPU;只能保证一个变量的原子性。
· 实战:高并发下LongAdder比AtomicLong性能更好(分段累加)。
第12题:Java内存模型(JMM)是什么?
核心回答:
· JMM是Java并发编程的规范,定义了线程工作内存和主内存之间的交互规则。
· 保证三大特性:原子性、可见性、有序性。
内存模型图:
CPU1 → 缓存 → 工作内存1
↕ (read/write/load/store等8种操作)
主内存(堆、静态变量等)
↕
CPU2 → 缓存 → 工作内存2
8种交互操作: lock、unlock、read、load、use、assign、store、write(需满足一定规则)。
开发经验:
· 理解JMM才能写出正确的并发代码。
· volatile、synchronized、final都是JMM的具体实现。
第13题:happens-before原则有哪些?
核心回答:
happens-before(先行发生原则):A操作的结果对B操作可见,则A happens-before B。
8条规则(必背):
- 程序次序规则:同一线程中,代码顺序前面的happens-before后面的。
- 管程锁定规则:unlock happens-before lock(同一个锁)。
- volatile规则:volatile写 happens-before 之后的volatile读。
- 线程启动规则:start() happens-before 该线程的任何操作。
- 线程终止规则:线程所有操作 happens-before join()返回。
- 线程中断规则:interrupt() happens-before 检测到中断。
- 对象终结规则:构造方法执行完 happens-before finalize()。
- 传递性:A happens-before B,B happens-before C,则A happens-before C。
开发经验:
· 面试能说出3条以上即可,全部说出是亮点。
· 这些规则是JVM保证可见性的依据。
第14题:什么是可重入锁?
核心回答:
· 同一线程可以多次获取同一把锁,不会死锁。
· synchronized和ReentrantLock都是可重入锁。
示例:
java
synchronized void methodA() {
methodB(); // 同一个线程再次获取锁,成功
}
synchronized void methodB() {}
底层原理:
· synchronized:monitor计数器,获取一次+1,释放-1。
· ReentrantLock:AQS的state,当前持有线程重入时state++。
开发经验:
· 递归方法必须用可重入锁,否则会死锁。
· 可重入锁让你在同步方法中调用另一个同步方法,这是常见的编程模式。
第15题:公平锁和非公平锁区别?
核心回答:
· 公平锁:按线程请求锁的顺序(FIFO)依次获取,不会插队。
· 非公平锁:新来的线程可能直接尝试获取锁,获取不到再排队。
性能对比:
· 非公平锁吞吐量更高(减少线程切换),但可能造成线程饥饿。
· 公平锁更公平,但性能略低。
底层原理:
· ReentrantLock fair = new ReentrantLock(true)公平,false非公平(默认)。
· 非公平锁在tryAcquire时先抢一次,失败再排队。
开发经验:
· 默认用非公平,除非业务必须顺序执行。
· 公平锁会让线程频繁在BLOCKED/RUNNABLE间切换,影响性能。
二、锁与同步篇(16-35)
第16题:synchronized底层实现原理?
核心回答:
· synchronized是基于monitor(监视器锁)实现的,每个对象都有一个monitor。
· 字节码层面:同步方法有ACC_SYNCHRONIZED标志;同步代码块有monitorenter和monitorexit指令。
锁升级过程(JDK1.6优化):
无锁 → 偏向锁 → 轻量级锁(自旋锁)→ 重量级锁(OS互斥量)
· 偏向锁:无竞争时,记录线程ID,直接获取,消除CAS。
· 轻量级锁:竞争不激烈时,用CAS尝试获取,不自旋太重。
· 重量级锁:竞争激烈时,阻塞线程,由OS调度。
底层原理:
· Java对象头中包含Mark Word,存储锁信息。
· 重量级锁时,Mark Word指向monitor对象的指针,monitor中有_owner、_EntryList、_WaitSet等结构。
开发经验:
· 锁升级是面试必考,要能画出对象头结构图。
· 实际开发中,锁竞争不激烈时synchronized性能并不差,JVM做了大量优化。
· 能用synchronized就别用ReentrantLock,除非需要高级功能。
第17题:偏向锁、轻量级锁、重量级锁详解?
核心回答:
偏向锁(Biased Locking):
· 场景:只有一个线程访问同步块。
· 原理:Mark Word记录线程ID,以后该线程进来直接获取,无需CAS。
· 撤销:当其他线程竞争时,撤销偏向锁,升级为轻量级锁。
· 注意:偏向锁在JDK15默认禁用,因为维护开销大于收益。
轻量级锁(Lightweight Locking):
· 场景:两个线程交替执行,无激烈竞争。
· 原理:线程在自己的栈帧中创建锁记录(Lock Record),用CAS将对象头指向锁记录。
· 失败:如果CAS失败,说明有竞争,自旋等待,自旋超过阈值升级重量级。
重量级锁(Heavyweight Locking):
· 场景:竞争激烈,自旋耗CPU。
· 原理:线程阻塞,进入等待队列,由OS调度。
· 开销:用户态→内核态切换,性能下降明显。
开发经验:
· 锁升级是单向的:偏向→轻量→重量,不能降级。
· 自旋次数在JDK1.6后是自适应的(根据上次自旋成功率动态调整)。
· 写代码时不用关心这些,但理解后能写出更高效的代码。
第18题:ReentrantLock和synchronized区别?
核心回答:
特性 synchronized ReentrantLock
锁实现 JVM层面(monitor) JDK层面(AQS)
可中断 ❌ 不可中断等待 ✅ lockInterruptibly()
超时获取 ❌ ✅ tryLock(timeout)
公平锁 ❌ 非公平 ✅ 可选择公平/非公平
多个条件 ❌ 只有一个waitSet ✅ 多个Condition
手动释放 自动 必须unlock()
性能 JDK1.6后接近 略优,但差距缩小
使用示例:
java
Lock lock = new ReentrantLock();
lock.lock();
try {
// 业务逻辑
} finally {
lock.unlock(); // 必须finally释放
}
开发经验:
· 优先用synchronized,除非需要可中断、超时、公平锁、多条件绑定。
· 用ReentrantLock一定要在finally中unlock,否则死锁。
· 条件绑定的典型场景:生产者消费者,多个等待队列(如仓库满了等生产者,仓库空了等消费者)。
第19题:读写锁ReentrantReadWriteLock?
核心回答:
· 读写锁维护一对锁:读锁(共享锁)和写锁(独占锁)。
· 规则:读-读共享,读-写互斥,写-写互斥。
内部结构:
· AQS的state高16位表示读锁计数,低16位表示写锁计数。
· 读锁可重入,写锁可重入。
使用示例:
java
ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
Lock readLock = rw.readLock();
Lock writeLock = rw.writeLock();
// 读操作用读锁,多个线程可同时读
readLock.lock();
try { // 读取数据 } finally { readLock.unlock(); }
// 写操作用写锁,互斥
writeLock.lock();
try { // 修改数据 } finally { writeLock.unlock(); }
开发经验:
· 读多写少的场景性能提升明显,如缓存。
· 注意锁降级:写锁降级为读锁是允许的,但读锁升级为写锁会死锁。
· 如果读锁被占用,写锁无法获取,可能导致写线程饥饿。
第20题:StampedLock是什么?
核心回答:
· StampedLock是JDK1.8引入的读写锁改进版,支持三种模式:写锁、读锁、乐观读。
· 乐观读:不加锁,读取后验证有没有写操作发生。
使用示例:
java
StampedLock lock = new StampedLock();
// 乐观读
long stamp = lock.tryOptimisticRead();
// 读取数据
if (!lock.validate(stamp)) {
// 有写操作,升级为读锁
stamp = lock.readLock();
try { // 重新读取 } finally { lock.unlockRead(stamp); }
}
// 写锁
stamp = lock.writeLock();
try { // 修改 } finally { lock.unlockWrite(stamp); }
底层原理:
· 使用邮戳(stamp)标记版本,每次写操作都会改变stamp。
· 性能优于ReentrantReadWriteLock,因为乐观读不阻塞写。
开发经验:
· 适合读远多于写的场景,且读操作轻量级。
· 不支持可重入,使用时需小心。
· 如果乐观读验证失败频率很高(写很多),性能反而不如普通读写锁。
第21题:什么是锁降级?
核心回答:
· 锁降级是指写锁降级为读锁:先获取写锁,再获取读锁,最后释放写锁。
· 目的是保证写操作对后续读操作的可见性。
示例:
java
ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
Lock readLock = rw.readLock();
Lock writeLock = rw.writeLock();
writeLock.lock();
try {
// 修改数据
readLock.lock(); // 获取读锁
} finally {
writeLock.unlock(); // 释放写锁,持有读锁
}
// 此时是读锁状态,可以继续读取
底层原理:
· 降级过程中,写锁和读锁同时持有,但不会死锁,因为读锁允许在当前持有写锁时获取。
· 不支持升级:持有读锁时获取写锁会死锁。
开发经验:
· 锁降级的目的是保证数据一致性:修改完数据后,后续读操作能看到最新值。
· 缓存更新场景常用:更新缓存后,用读锁保护缓存读取。
第22题:死锁四个必要条件?
核心回答:
死锁必须同时满足四个条件:
- 互斥条件:资源一次只能被一个线程占用。
- 占有并等待:线程持有资源,等待其他资源。
- 不可剥夺条件:资源只能由持有者释放,不能被抢占。
- 循环等待条件:线程间形成循环等待链。
示例:
java
// 线程1
synchronized(A) { synchronized(B) { } }
// 线程2
synchronized(B) { synchronized(A) { } }
开发经验:
· 破坏循环等待(按固定顺序获取锁)是最常用的解法。
· jstack可检测死锁,会输出"Found one Java-level deadlock"。
第23题:如何预防死锁?
核心回答:
破坏四个必要条件之一:
· 破坏互斥:很难,有些资源就是独占。
· 破坏占有并等待:一次性申请所有资源。
· 破坏不可剥夺:使用tryLock(timeout)。
· 破坏循环等待:约定锁获取顺序(如按hashCode排序)。
代码示例:
java
Lock lockA = new ReentrantLock();
Lock lockB = new ReentrantLock();
// 使用tryLock避免死锁
if (lockA.tryLock(1, TimeUnit.SECONDS)) {
try {
if (lockB.tryLock(1, TimeUnit.SECONDS)) {
try { // 业务 } finally { lockB.unlock(); }
}
} finally { lockA.unlock(); }
}
开发经验:
· 实际工程中,规范加锁顺序是最有效的方法。
· 使用jstack分析线上死锁,及时修复。
· 阿里开发规范:多个锁时,按统一顺序获取。
第24题:活锁和死锁区别?
核心回答:
· 死锁:线程互相等待对方释放资源,都阻塞,无法继续。
· 活锁:线程不断改变状态尝试执行,但始终无法推进,像两个人在窄路上互相让路,结果还是堵着。
典型场景:
· 两个线程都使用tryLock,失败后释放自己的锁并重试,但每次都同时重试,导致互相谦让永远不成功。
解决方案:
· 引入随机退避(随机等待一段时间再重试)。
· 或使用指数退避(等待时间逐渐增加)。
开发经验:
· 活锁比死锁更隐蔽,因为线程没有阻塞,看起来还在运行,但业务无推进。
· 监控业务指标(如处理量)可发现活锁。
第25题:什么是锁粗化?
核心回答:
· 锁粗化是JVM的优化技术:将多个连续的锁操作合并为一次。
· 例如循环内反复加锁/解锁,JVM会把锁移到循环外。
示例:
java
// 优化前:每次循环加锁解锁
for (int i = 0; i < 100; i++) {
synchronized(this) { count++; }
}
// 优化后:JVM粗化为锁一次
synchronized(this) {
for (int i = 0; i < 100; i++) { count++; }
}
底层原理:
· JIT编译器在运行时分析,自动优化。
· 不会影响语义正确性。
开发经验:
· 虽然JVM会优化,但写代码时应该主动避免循环内加锁,养成良好的习惯。
· 锁粗化对性能提升明显,特别是循环次数很多时。
第26题:什么是锁消除?
核心回答:
· 锁消除是JVM的优化技术:检测到不可能发生竞争时,直接去掉锁。
· 基于逃逸分析:如果锁对象不会逃逸出当前线程,则消除锁。
示例:
java
public void method() {
// StringBuffer的append是synchronized的
StringBuffer sb = new StringBuffer();
sb.append("a").append("b"); // sb不会逃逸,JVM消除锁
}
底层原理:
· 逃逸分析证明对象只在当前线程使用,则加锁无意义,JVM消除。
开发经验:
· 写代码时不需要主动关心,JVM会自动优化。
· 但如果用StringBuffer作为成员变量(逃逸),锁就不会消除。
第27题:什么是自旋锁?
核心回答:
· 自旋锁:线程不放弃CPU,在一个循环中不断尝试获取锁(CAS操作)。
· 优点:避免线程切换开销(用户态→内核态)。
· 缺点:长时间自旋浪费CPU。
自适应自旋:
· JDK1.6引入,自旋时间由上次自旋成功率决定。
· 上次成功,这次多自旋一会;上次失败,这次少自旋或直接挂起。
底层原理:
· 轻量级锁的实现就包含自旋。
· 自旋次数由JVM参数控制:-XX:PreBlockSpin。
开发经验:
· 自旋锁适合锁持有时间短的场景。
· 如果锁持有时间长,自旋反而浪费CPU,建议用重量级锁。
· 实际开发中,这些由JVM自适应调节,我们不用手动设置。
第28题:什么是重入锁的公平性?
核心回答:
· ReentrantLock的公平性由构造参数fair决定:
· true:公平锁,按FIFO顺序获取。
· false:非公平锁,允许插队(默认)。
公平锁的实现:
· tryAcquire时,先检查hasQueuedPredecessors()(队列中是否有前驱)。
· 如果有前驱,即使锁空闲也排队。
非公平锁的实现:
· tryAcquire时,直接尝试CAS获取锁,成功则插队。
· 失败才排队。
性能差异:
· 非公平锁减少了线程挂起/唤醒的开销,吞吐量更高。
· 公平锁可能导致频繁的上下文切换。
开发经验:
· 默认用非公平锁,除非业务要求严格顺序(如交易系统)。
· 非公平锁的"插队"在大多数场景下是可接受的,因为性能更重要。
第29题:Condition接口作用?
核心回答:
· Condition替代wait/notify,提供更灵活的等待/通知机制。
· 一个Lock可以绑定多个Condition,实现精确唤醒。
使用示例:
java
Lock lock = new ReentrantLock();
Condition notEmpty = lock.newCondition();
Condition notFull = lock.newCondition();
// 生产者
lock.lock();
try {
while (queue.isFull()) notFull.await();
queue.put(data);
notEmpty.signal();
} finally { lock.unlock(); }
// 消费者
lock.lock();
try {
while (queue.isEmpty()) notEmpty.await();
data = queue.take();
notFull.signal();
} finally { lock.unlock(); }
与wait/notify对比:
· Condition可以精确唤醒特定线程(如只唤醒生产者)。
· wait/notify只能随机唤醒一个或全部。
开发经验:
· 多个Condition让代码更清晰,避免notifyAll无效唤醒。
· 生产者消费者场景强烈推荐使用Condition。
第30题:LockSupport的park/unpark?
核心回答:
· LockSupport.park():阻塞当前线程。
· LockSupport.unpark(thread):唤醒指定线程。
· 与wait/notify不同:不需要持有锁,且可以先unpark再park(提前发放许可)。
底层原理:
· 基于许可(permit)机制,类似信号量,但许可最多为1。
· unpark提前给了一个许可,park会消耗许可直接返回,不阻塞。
使用场景:
· AQS底层用park/unpark实现线程阻塞和唤醒。
· 比wait/notify更灵活、更安全。
开发经验:
· 普通开发不直接使用,但理解它对理解AQS有帮助。
· park会响应中断,但不抛出InterruptedException,需要用Thread.interrupted()检测。
第31题:Semaphore信号量?
核心回答:
· Semaphore控制同时访问的线程数,通过许可证(permits)管理。
· 获取许可用acquire(),释放用release()。
使用示例:
java
// 最多3个线程同时访问
Semaphore semaphore = new Semaphore(3);
semaphore.acquire();
try { // 访问资源 } finally { semaphore.release(); }
底层原理:
· 基于AQS共享模式,state表示剩余许可证数。
· 公平/非公平可选(类似ReentrantLock)。
开发经验:
· 限流场景:限制数据库连接数、接口QPS。
· 注意释放许可证必须在finally中,否则资源耗尽。
· tryAcquire()非阻塞版本可用于超时控制。
第32题:CountDownLatch?
核心回答:
· CountDownLatch让一个或多个线程等待其他线程完成。
· 计数器递减到0时,等待的线程被唤醒。
使用示例:
java
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
new Thread(() -> {
// 执行任务
latch.countDown();
}).start();
}
latch.await(); // 等待3个任务完成
底层原理:
· 基于AQS共享模式,state初始为N。
· countDown()释放共享锁,state-1;await()获取共享锁,state=0时才成功。
开发经验:
· 只能使用一次,计数器归零后不能重置。
· 适合主线程等待多个子线程完成后再汇总。
· 与CyclicBarrier区别:CountDownLatch是一等待多完成,CyclicBarrier是多相互等待。
第33题:CyclicBarrier?
核心回答:
· CyclicBarrier让一组线程互相等待,所有线程都到达屏障后,一起继续执行。
· 可重用(reset后重新使用)。
使用示例:
java
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("所有线程到达,执行最后操作");
});
for (int i = 0; i < 3; i++) {
new Thread(() -> {
// 执行阶段一
barrier.await(); // 等待其他人
// 执行阶段二
}).start();
}
与CountDownLatch区别:
CountDownLatch CyclicBarrier
一等待多完成 多相互等待
不可重用 可重用(reset)
基于AQS 基于ReentrantLock+Condition
开发经验:
· 适合多阶段计算,每个阶段所有线程完成后才进入下一阶段。
· 注意:一个线程await超时或中断,其他线程会抛出BrokenBarrierException。
第34题:Exchanger?
核心回答:
· Exchanger用于两个线程之间交换数据。
· 双方都调用exchange(),交换数据后继续执行。
使用示例:
java
Exchanger<String> exchanger = new Exchanger<>();
// 线程1
String data1 = "数据A";
String data2 = exchanger.exchange(data1);
// 线程2
String data1 = "数据B";
String data2 = exchanger.exchange(data1);
// 此时data2 = "数据A", data2 = "数据B"
底层原理:
· 内部使用CAS+自旋,如果没有配对的线程,会阻塞等待。
· 支持超时版本:exchange(V, timeout, unit)。
开发经验:
· 应用场景较少,如两个线程数据校对、生产者-消费者单对单。
· 如果超过两个线程,Exchanger无法使用。
第35题:Phaser?
核心回答:
· Phaser是可重用的同步屏障,支持多阶段任务。
· 每个阶段(phase)等待所有参与者到达,然后进入下一阶段。
使用示例:
java
Phaser phaser = new Phaser(3); // 3个参与者
for (int i = 0; i < 3; i++) {
new Thread(() -> {
// 阶段1
phaser.arriveAndAwaitAdvance();
// 阶段2
phaser.arriveAndAwaitAdvance();
}).start();
}
// 主线程等待所有阶段完成
phaser.awaitAdvance(0); // 等待阶段0完成
关键方法:
· arriveAndAwaitAdvance():到达并等待其他人。
· arriveAndDeregister():到达并注销(减少参与者)。
· getPhase():获取当前阶段号。
开发经验:
· 比CyclicBarrier更灵活,可动态增减参与者。
· 适合复杂分阶段并行任务,如数据导入多个阶段。
· 实际项目中用CompletableFuture更多,Phaser相对冷门。
三、并发容器篇(36-50)
好的,我们继续第三部分:并发容器篇(36-50题)。这部分是实际开发中最常用的工具,面试中ConcurrentHashMap几乎是必考中的必考,一定要吃透。
第36题:ConcurrentHashMap底层结构?
核心回答:
JDK 1.7版本(分段锁):
· 数据结构:Segment数组 + HashEntry数组 + 链表
· Segment继承ReentrantLock,每个Segment独立加锁
· 默认16个Segment,支持并发度16
JDK 1.8+版本(CAS + synchronized):
· 数据结构:Node数组 + 链表/红黑树
· 取消分段锁,改为桶粒度锁(只锁链表/红黑树头节点)
· 写操作:CAS + synchronized锁定头节点
· 读操作:volatile保证可见性,无锁
底层原理图:
JDK1.8 ConcurrentHashMap:
┌─────────────────────────────────────┐
│ Node[] table (volatile) │
│ ┌─────┬─────┬─────┬─────┬─────┐ │
│ │ 桶0 │ 桶1 │ 桶2 │ 桶3 │ ... │ │
│ └──┬──┴──┬──┴──┬──┴──┬──┴─────┘ │
│ │ │ │ │ │
│ 链表 红黑树 链表 空 │
│ (<=8) (>8) │
└─────────────────────────────────────┘
开发经验:
· 读操作完全无锁,所以性能极高
· 实际项目中首选ConcurrentHashMap,不推荐Hashtable
· size()方法在1.8中用了sumCount(),用mappingCount()可获取更精确的长整型值
第37题:ConcurrentHashMap如何保证线程安全?
核心回答:
写操作(put):
java
final V putVal(K key, V value, boolean onlyIfAbsent) {
// 1. 计算hash
// 2. 如果table为空,初始化(CAS + 自旋)
// 3. 如果桶为空,CAS直接插入(无锁)
// 4. 如果桶不为空,synchronized锁住头节点
// 5. 如果是链表,遍历插入;如果是红黑树,树插入
// 6. 检查是否需要扩容
}
读操作(get):
· Node的value和next都用volatile修饰
· 读取时直接访问,不加锁
· 扩容时,读操作会访问ForwardingNode,转到新table读取
核心机制:
- CAS:用于桶的初始化、插入第一个节点
- synchronized:锁住桶的头节点,只影响当前桶
- volatile:保证table、Node的可见性
- Unsafe:直接操作内存,提高效率
开发经验:
· 并发度极高,因为只锁一个桶,其他桶不受影响
· 即使扩容时,读操作也不阻塞,读取旧table或新table
· 这是面试必考题,要能把上述流程讲清楚
第38题:ConcurrentHashMap和Hashtable区别?
核心回答:
对比项 Hashtable ConcurrentHashMap
锁粒度 全表锁(方法级synchronized) 桶锁(1.8)或段锁(1.7)
并发度 低,同一时间只有一个线程操作 高,不同桶可并发操作
空键/空值 ❌ 不允许 ❌ 不允许(1.8)
迭代器 快速失败(fail-fast) 弱一致(fail-safe)
性能 差 优
关键区别:
· Hashtable的put/get都加synchronized,竞争激烈
· ConcurrentHashMap的get无锁,put只锁一个桶
开发经验:
· Hashtable已经过时,生产环境禁止使用
· HashMap线程不安全,ConcurrentHashMap线程安全,根据场景选择
· 即使是单线程场景,如果不需要线程安全,用HashMap比ConcurrentHashMap更快
第39题:ConcurrentHashMap扩容机制?
核心回答:
扩容触发条件:
· 元素个数 > sizeCtl(阈值 = 容量 * 负载因子)
· 链表长度 > 8且数组长度 < 64(树化前先扩容)
扩容流程(1.8):
- 创建新table,容量为原来的2倍
- 多线程协同扩容,每个线程负责一部分旧桶
- 迁移时,旧桶节点被移动到新table
- 迁移完成后,旧桶标记为ForwardingNode(转发节点)
- 全部迁移完成,新table替换旧table
核心变量:
· sizeCtl:扩容控制标志(负数表示正在扩容)
· transferIndex:扩容进度(从后往前分配任务)
· ForwardingNode:扩容期间占位符,指向新table
开发经验:
· 扩容期间,读操作会通过ForwardingNode去新table读取
· 写入操作会帮助扩容(扩容期间新数据放入新table)
· 这是P6面试亮点:能讲清楚多线程协同扩容,说明对源码有深入研究
第40题:CopyOnWriteArrayList?
核心回答:
· 读操作无锁,写操作复制新数组并替换
· 适用:读多写极少的场景
核心源码:
java
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
// 复制新数组(开销大)
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements); // volatile赋值
return true;
} finally {
lock.unlock();
}
}
public E get(int index) {
return getArray()[index]; // 无锁
}
优缺点:
· 优点:读操作极快,适合读多写少
· 缺点:写操作复制整个数组,内存占用大;数据一致性弱(读旧数据)
开发经验:
· 典型场景:黑名单、配置信息、路由表
· 写入频繁的场景不要用,会导致频繁GC
· 迭代器是快照风格,不会抛出ConcurrentModificationException
第41题:ConcurrentLinkedQueue?
核心回答:
· 无界非阻塞队列,基于链表实现
· 使用CAS实现线程安全,无锁化
核心数据结构:
java
private static class Node<E> {
volatile E item;
volatile Node<E> next;
}
private transient volatile Node<E> head;
private transient volatile Node<E> tail;
核心方法:
· offer(E e):队尾插入(CAS更新tail)
· poll():队首移除(CAS更新head)
· peek():查看队首不删除
· size():遍历计算,非精确值
开发经验:
· 适合高吞吐量场景,不阻塞
· 无法使用阻塞等待(如take),需要手动轮询
· 无界队列,需注意内存溢出风险
· 实际中,BlockingQueue更常用(需要阻塞功能)
第42题:BlockingQueue实现类有哪些?
核心回答:
实现类 有界/无界 数据结构 特点
ArrayBlockingQueue 有界 数组 先进先出,公平锁可选
LinkedBlockingQueue 可选有界 链表 默认Integer.MAX_VALUE
PriorityBlockingQueue 无界 二叉堆 按优先级出队
SynchronousQueue 无容量 无存储 每个put必须等take
DelayQueue 无界 优先级队列 延迟出队
LinkedTransferQueue 无界 链表 支持transfer
使用场景:
· ArrayBlockingQueue:确定容量,公平性要求高
· LinkedBlockingQueue:线程池默认(FixedThreadPool)
· SynchronousQueue:CachedThreadPool使用
· DelayQueue:定时任务、缓存过期
开发经验:
· 线程池中workQueue就是BlockingQueue,选型很重要
· 无界队列要小心OOM(CachedThreadPool就是例子)
第43题:ArrayBlockingQueue和LinkedBlockingQueue区别?
核心回答:
对比项 ArrayBlockingQueue LinkedBlockingQueue
数据结构 数组(循环队列) 链表
容量 必须指定,有界 默认无界(Integer.MAX_VALUE)
锁分离 共用一把锁(put/take同一锁) 两把锁(putLock/takeLock)
内存分配 预分配 动态分配
吞吐量 较低 较高
锁分离机制:
· ArrayBlockingQueue:put和take共用ReentrantLock
· LinkedBlockingQueue:put用putLock,take用takeLock,可同时put和take
开发经验:
· 高并发场景LinkedBlockingQueue性能更好(锁分离)
· 但注意默认无界,需指定容量防止OOM
· ArrayBlockingQueue适合容量固定、公平性要求高的场景
第44题:SynchronousQueue?
核心回答:
· 不存储元素的队列,每个put操作必须等待一个take操作
· 内部容量为0,生产者直接传递给消费者
两种模式:
· 公平模式:使用TransferQueue(FIFO),保证顺序
· 非公平模式:使用TransferStack(LIFO),性能更高
使用场景:
· CachedThreadPool的workQueue就是SynchronousQueue
· 每个任务提交后,立即交给空闲线程执行,没有空闲线程则创建新线程
开发经验:
· 一般不直接使用,但理解它对理解线程池很重要
· 配合CachedThreadPool使用,任务传递效率高
· 如果生产者速度远大于消费者,会导致线程无限创建
第45题:DelayQueue?
核心回答:
· 延迟队列,元素必须实现Delayed接口
· 只有延迟时间到期才能被take获取
· 内部使用PriorityQueue + ReentrantLock
Delayed接口:
java
public interface Delayed extends Comparable<Delayed> {
long getDelay(TimeUnit unit); // 剩余延迟时间
}
使用示例:
java
class Task implements Delayed {
private long executeTime;
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(executeTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
}
@Override
public int compareTo(Delayed o) {
return Long.compare(executeTime, ((Task) o).executeTime);
}
}
开发经验:
· 典型场景:定时任务、缓存过期检测、订单超时取消
· 元素必须在到期后才能出队,适用于需要延迟处理的场景
· 无界队列,注意内存占用
第46题:ConcurrentSkipListMap?
核心回答:
· 跳表(Skip List)实现的有序并发Map
· 线程安全,支持ConcurrentNavigableMap接口
· 替代ConcurrentTreeMap(不存在)
跳表结构:
Level 3: 1 ---------------> 9
Level 2: 1 ------> 5 ------> 9
Level 1: 1 -> 3 -> 5 -> 7 -> 9
· 多层链表,上层是下层的"快速通道"
· 查找时间复杂度 O(log n)
与ConcurrentHashMap对比:
· ConcurrentHashMap:无序,性能更高
· ConcurrentSkipListMap:有序,但性能稍低
开发经验:
· 需要有序并发Map时使用(如排名系统)
· 比TreeMap加synchronized性能好
· 实际使用频率低于ConcurrentHashMap
第47题:阻塞队列的put/take和offer/poll区别?
核心回答:
方法 队列满时 队列空时
put(E) 阻塞等待 -
take() - 阻塞等待
offer(E) 返回false -
poll() - 返回null
offer(E, timeout) 超时返回false -
poll(timeout) - 超时返回null
add(E) 抛异常 -
remove() - 抛异常
四套接口:
- 抛异常:add、remove、element
- 返回特殊值:offer、poll、peek
- 阻塞:put、take
- 超时:offer(timeout)、poll(timeout)
开发经验:
· 生产者消费者模式用put/take最安全
· 非阻塞场景用offer/poll避免线程挂起
· 生产环境多用超时版本,避免永久阻塞
第48题:什么是伪共享?
核心回答:
· 伪共享(False Sharing):多个变量在同一个缓存行(Cache Line,通常64字节),一个线程修改其中一个变量,会导致整个缓存行失效,其他线程的变量也被失效,引起性能下降。
示例:
java
class Data {
volatile long a; // 线程1频繁修改
volatile long b; // 线程2频繁修改
// a和b可能在同一个缓存行,导致互相影响
}
解决方案: 缓存行填充
java
@Contended // JDK1.8注解,自动填充
class Data {
volatile long a;
volatile long b;
}
底层原理:
· CPU缓存一致性协议(MESI),以缓存行为单位同步
· 伪共享会导致"缓存行颠簸",性能下降严重
开发经验:
· 高性能框架如Disruptor特别关注伪共享
· @Contended注解需要JVM参数开启
· 实际开发中,如果发现多线程操作相邻变量性能异常,考虑伪共享
第49题:LongAdder和AtomicLong区别?
核心回答:
对比项 AtomicLong LongAdder
原理 CAS自旋 分段累加(Cell数组)
性能(高并发写) 低(CAS竞争激烈) 高(分散到多个Cell)
性能(读) 高 低(需要累加所有Cell)
内存占用 小 大(Cell数组)
适用场景 写少读多 写多读少
LongAdder原理:
java
// 类似分段计数
Cell[] cells; // 每个线程映射到不同Cell
long base; // 无竞争时使用
// 累加时,优先用CAS更新base,失败则分散到Cell
// sum()时累加所有Cell + base
开发经验:
· 高并发统计场景(如计数器、QPS统计)用LongAdder
· 需要精确值且读操作多时用AtomicLong
· 实际项目中,LongAdder是更好的选择(读少写多场景更常见)
第50题:ThreadLocal原理?
核心回答:
数据结构:
java
ThreadLocalMap(每个线程独有)
├── key: ThreadLocal对象(弱引用)
└── value: 存储的值
核心原理:
- 每个线程维护一个ThreadLocalMap
- set(value):以当前ThreadLocal为key,存入当前线程的Map
- get():从当前线程的Map中获取值
- 不同线程访问同一个ThreadLocal,得到各自的值
内存泄漏问题:
· ThreadLocal使用弱引用作为key
· 如果ThreadLocal不再被引用,GC时key被回收,但value仍存在(强引用)
· 导致value无法被回收,造成内存泄漏
解决方案:
java
try {
// 使用ThreadLocal
} finally {
threadLocal.remove(); // 必须手动remove
}
开发经验:
· 典型场景:用户上下文传递、数据库连接管理、SimpleDateFormat线程安全
· 阿里开发规范:必须remove,否则可能内存泄漏
· 线程池场景尤其危险,因为线程被复用,ThreadLocalMap一直存在
第三部分总结: 这15题涵盖了Java并发容器的核心,其中:
· ConcurrentHashMap是重中之重,要深入研究1.8源码
· BlockingQueue是线程池的基础,要理解各种实现类的区别
· ThreadLocal内存泄漏是面试必问,要能说清弱引用和remove的必要性
四、线程池篇(51-70)
第51题:为什么要用线程池?
核心回答:
三大好处:
- 降低资源消耗:复用已创建的线程,避免频繁创建/销毁的开销
- 提高响应速度:任务来了直接执行,不用等待创建线程
- 提高线程管理性:统一分配、调优、监控,控制并发数
对比:
java
// ❌ 不用线程池:每次new线程,开销大
new Thread(() -> doTask()).start();
// ✅ 用线程池:复用线程,性能好
executor.execute(() -> doTask());
底层原理:
· 线程创建涉及JVM和OS交互,分配栈内存(默认1MB),开销大
· 线程池通过线程复用机制,减少频繁GC和上下文切换
开发经验:
· 阿里开发规范:线程池不允许用Executors创建,要用ThreadPoolExecutor
· 没有线程池的项目,高并发下必然OOM或CPU飙升
· 不同业务要用不同的线程池隔离(如订单、用户、日志分开)
第52题:ThreadPoolExecutor核心参数?
核心回答:
java
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 空闲线程存活时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 任务队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler // 拒绝策略
)
参数详解:
· corePoolSize:核心线程数,即使空闲也不会被回收(除非设置allowCoreThreadTimeOut)
· maximumPoolSize:最大线程数,队列满时才创建新线程到此上限
· keepAliveTime:非核心线程空闲多久被回收
· workQueue:任务存放队列,阻塞队列
· threadFactory:创建线程的工厂,可自定义线程名
· handler:队列满且线程数达最大值时的拒绝策略
开发经验:
· corePoolSize和maximumPoolSize是调优核心,需根据业务压测确定
· workQueue选型直接影响线程池行为(有界/无界、阻塞/非阻塞)
第53题:线程池执行流程?
核心回答:
完整执行流程(必画图):
提交任务
↓
① 核心线程数 < corePoolSize?
↓ 是 ↓ 否
创建新线程执行 ② 任务队列是否已满?
↓ 否 ↓ 是
放入队列等待 ③ 线程数 < maximumPoolSize?
↓ 是 ↓ 否
创建新线程执行 ④ 执行拒绝策略
文字版:
- 当前线程数 < corePoolSize → 创建新核心线程执行
- 当前线程数 ≥ corePoolSize → 任务放入workQueue
- workQueue已满 → 创建新线程执行(直到maximumPoolSize)
- 线程数 = maximumPoolSize 且 队列满 → 执行拒绝策略
开发经验:
· 这个流程是面试必考,要能画图+口述
· 不同线程池(Fixed/Cached)就是参数不同导致行为不同
· 理解流程才能正确配置线程池参数
第54题:几种常见线程池?
核心回答:
Executors提供的四种:
线程池 corePoolSize maxPoolSize workQueue 特点
FixedThreadPool n n LinkedBlockingQueue 固定线程数,无界队列
CachedThreadPool 0 Integer.MAX_VALUE SynchronousQueue 可缓存,无限线程
SingleThreadExecutor 1 1 LinkedBlockingQueue 单线程串行
ScheduledThreadPool n Integer.MAX_VALUE DelayedWorkQueue 定时/延迟任务
底层源码:
java
// FixedThreadPool
return new ThreadPoolExecutor(n, n, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
// CachedThreadPool
return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
开发经验:
· 不推荐用Executors创建,因为无界队列可能OOM
· 唯一可用的是ScheduledThreadPool,但要控制corePoolSize
· 实际项目全部手动new ThreadPoolExecutor
第55题:为什么不建议Executors创建?
核心回答:
三大隐患:
- FixedThreadPool / SingleThreadExecutor:
· 使用无界队列 LinkedBlockingQueue(容量Integer.MAX_VALUE)
· 任务堆积可能OOM
java
// 如果任务速度 > 消费速度,内存持续增长
newFixedThreadPool(10).execute(() -> {
while(true) { // 任务处理慢 }
});
// → 队列无限增长,OOM
- CachedThreadPool / ScheduledThreadPool:
· 最大线程数为Integer.MAX_VALUE
· 线程无限创建,可能导致OOM或CPU耗尽
- 不利于监控:
· 没有自定义线程名,排障困难
· 无法统一管理参数
开发经验:
· 阿里规范明确禁止Executors创建线程池
· 必须使用ThreadPoolExecutor,参数显式设置
· 有界队列 + 合理拒绝策略,是生产标配
第56题:四种拒绝策略?
核心回答:
策略 行为 风险
AbortPolicy(默认) 抛出RejectedExecutionException 调用方需捕获处理
CallerRunsPolicy 由调用者线程直接执行 阻塞调用方,削峰
DiscardPolicy 直接丢弃任务,无通知 任务丢失不可感知
DiscardOldestPolicy 丢弃队列最老任务,提交当前任务 可能丢失重要任务
自定义拒绝策略:
java
new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 记录日志、告警、降级、放入MQ
log.error("线程池满,任务被拒绝:{}", r);
// 降级处理:返回默认值
}
};
开发经验:
· 生产用CallerRunsPolicy:由调用者执行,相当于天然限流
· 或自定义策略:记录日志 + 告警 + 降级
· 不要用DiscardPolicy,任务丢失无法感知
· 配合有界队列使用,拒绝策略才有意义
第57题:如何自定义拒绝策略?
核心回答:
完整示例:
java
ThreadPoolExecutor executor = new ThreadPoolExecutor(
10, 50, 60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1000),
new NamedThreadFactory("biz-"),
new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 1. 记录日志(重要)
log.warn("线程池已满,任务被拒绝,当前活跃线程数:{},队列大小:{}",
executor.getActiveCount(), executor.getQueue().size());
// 2. 告警(接入监控)
alertService.send("线程池满告警", executor);
// 3. 降级处理
if (r instanceof Task) {
Task task = (Task) r;
// 存入MQ,异步重试
mq.send(task);
// 或直接返回默认值
task.setDefaultResult();
}
// 4. 或者使用CallerRunsPolicy让调用者执行
// 但不要直接执行,可能压垮调用者
}
}
);
开发经验:
· 降级策略要业务相关,不能一概而论
· 日志中要包含线程池状态(活跃数、队列数、完成任务数)
· 接入监控系统,设置告警阈值
第58题:线程池状态有哪些?
核心回答:
5种状态(存储在ctl的高3位):
状态 值 说明
RUNNING -1 << COUNT_BITS 运行中,接受新任务,处理队列任务
SHUTDOWN 0 不接新任务,处理队列任务
STOP 1 不接新任务,不处理队列任务,中断进行中任务
TIDYING 2 所有任务已终止,即将执行terminated()
TERMINATED 3 terminated()执行完成
状态流转图:
RUNNING
↓ shutdown()
SHUTDOWN
↓ shutdownNow()
STOP
↓ (任务全部完成)
TIDYING
↓ terminated()
TERMINATED
开发经验:
· shutdown()是优雅关闭,shutdownNow()是强制关闭
· 判断线程池状态用isShutdown()和isTerminated()
· 实际生产中,优雅停机用shutdown() + awaitTermination(timeout)
第59题:shutdown和shutdownNow区别?
核心回答:
对比 shutdown() shutdownNow()
接受新任务 ❌ 不再接受 ❌ 不再接受
处理队列任务 ✅ 执行完所有队列任务 ❌ 放弃队列任务
进行中任务 ✅ 继续执行 ⚠️ 尝试中断(interrupt)
返回值 void List(未执行的任务)
状态变更 RUNNING → SHUTDOWN RUNNING → STOP
使用示例:
java
// 优雅停机
executor.shutdown();
try {
// 等待30秒,让已有任务执行完
if (!executor.awaitTermination(30, TimeUnit.SECONDS)) {
// 超时,强制关闭
executor.shutdownNow();
// 再等10秒,等待中断响应
if (!executor.awaitTermination(10, TimeUnit.SECONDS)) {
log.error("线程池未能正常关闭");
}
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
开发经验:
· 生产环境必须实现优雅停机,不能直接shutdownNow(数据丢失)
· 停机流程:shutdown → awaitTermination → shutdownNow(超时兜底)
· 任务中要响应中断(Thread.interrupted()检测)
第60题:如何合理设置线程池大小?
核心回答:
公式:
· CPU密集型:corePoolSize = CPU核心数 + 1
· IO密集型:corePoolSize = CPU核心数 * 2(或更多)
深入公式(考虑阻塞系数):
最优线程数 = CPU核心数 / (1 - 阻塞系数)
· 阻塞系数 = 阻塞时间 / (阻塞时间 + CPU计算时间)
· 例如:50%阻塞 → 线程数 = 核心数 / 0.5 = 2倍
实际调优方法:
- 通过压测确定最佳线程数
- 从核心数*2开始,逐步增加,观察TPS和响应时间
- 找到TPS最高且CPU利用率在70-80%的线程数
开发经验:
· 公式只是起点,压测才是王道
· 不要设置过大(上下文切换)或过小(CPU浪费)
· 不同业务类型(订单、报表、日志)分开配置
· 数据库连接池大小也要配合调整
第61题:线程池中的线程工厂?
核心回答:
作用: 创建线程,可自定义线程名称、优先级、守护状态、异常处理器。
实现:
java
public class NamedThreadFactory implements ThreadFactory {
private final String namePrefix;
private final AtomicInteger threadNumber = new AtomicInteger(1);
public NamedThreadFactory(String namePrefix) {
this.namePrefix = namePrefix;
}
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r, namePrefix + "-" + threadNumber.getAndIncrement());
t.setUncaughtExceptionHandler((thread, e) -> {
log.error("线程{}发生未捕获异常", thread.getName(), e);
});
// 设置为非守护线程(重要)
t.setDaemon(false);
return t;
}
}
开发经验:
· 必须自定义线程名(如biz-pool-1),排障必备
· 使用jstack时,有名字的线程一目了然
· 不要设置守护线程(守护线程finally不执行)
· 设置UncaughtExceptionHandler,捕获异常
第62题:如何监控线程池?
核心回答:
内置监控方法:
java
// 获取线程池状态
int activeCount = executor.getActiveCount(); // 活跃线程数
long taskCount = executor.getTaskCount(); // 总任务数
long completedTaskCount = executor.getCompletedTaskCount(); // 完成任务数
int queueSize = executor.getQueue().size(); // 队列大小
long poolSize = executor.getPoolSize(); // 当前线程数
int largestPoolSize = executor.getLargestPoolSize(); // 历史最大线程数
自定义监控:
java
public class MonitorThreadPool extends ThreadPoolExecutor {
private final Logger log = LoggerFactory.getLogger(getClass());
@Override
protected void beforeExecute(Thread t, Runnable r) {
// 记录开始时间
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
// 记录耗时、上报监控
}
// 定时打印状态
public void printStatus() {
log.info("活跃:{},队列:{},已完成:{}",
getActiveCount(), getQueue().size(), getCompletedTaskCount());
}
}
开发经验:
· 接入Prometheus + Grafana做可视化监控
· 设置告警:队列积压 > 阈值、拒绝次数 > 0、活跃线程持续高峰
· 监控是P6必备能力,能说出监控方案加分
第63题:线程池死锁问题?
核心回答:
场景: 任务A等待任务B完成,但B也在A所在的线程池队列中等待。
示例:
java
ExecutorService pool = Executors.newFixedThreadPool(2);
pool.submit(() -> {
Future<String> future = pool.submit(() -> "hello");
String result = future.get(); // 等待子任务
// 如果所有线程都在等待子任务,死锁
});
解决方案:
- 不同业务用不同线程池(任务隔离)
- 增加核心线程数,确保有线程处理子任务
- 避免任务提交到自己所在的线程池
开发经验:
· 异步编排时要注意任务依赖关系
· 使用CompletableFuture时,要指定独立的线程池
· 这是常见生产事故,务必重视
第64题:提交任务execute和submit区别?
核心回答:
对比 execute() submit()
参数 Runnable Runnable 或 Callable
返回值 void Future
异常处理 直接抛出 被Future.get()封装抛出
异常吞没 ❌ 不会吞没 ✅ 会被吞没(需get获取)
重要区别 - 异常处理:
java
// execute:异常直接抛到控制台
executor.execute(() -> { throw new RuntimeException("error"); });
// → 控制台打印异常栈
// submit:异常被封装在Future中
Future<?> future = executor.submit(() -> { throw new RuntimeException("error"); });
// → 没有异常输出,静默失败!
future.get(); // 这里才会抛出ExecutionException
开发经验:
· 用submit时,一定要调用Future.get() 或设置超时
· 或用submit提交Runnable时,自定义UncaughtExceptionHandler
· 不需要结果时用execute,避免异常被吞没
第65题:Future和FutureTask?
核心回答:
Future(接口):
· 表示异步计算的结果
· 提供方法:cancel(), isCancelled(), isDone(), get()
FutureTask(实现类):
· 实现了RunnableFuture接口(Runnable + Future)
· 既可作为任务提交,又可获取结果
使用示例:
java
// 方式1:直接提交Callable
Future<String> future = executor.submit(() -> "result");
String result = future.get(); // 阻塞
// 方式2:用FutureTask包装
FutureTask<String> futureTask = new FutureTask<>(() -> "result");
executor.submit(futureTask);
String result = futureTask.get();
// 方式3:线程直接执行
new Thread(futureTask).start();
底层原理:
· FutureTask内部用AQS实现等待/唤醒
· 状态流转:NEW → COMPLETING → NORMAL / EXCEPTIONAL / CANCELLED
开发经验:
· get()是阻塞的,要设置超时
java
try {
future.get(5, TimeUnit.SECONDS);
} catch (TimeoutException e) {
future.cancel(true); // 取消任务
}
第66题:CompletableFuture?
核心回答:
CompletableFuture是Java 8引入的异步编程利器,实现了Future和CompletionStage接口,支持回调驱动的链式编程。
核心优势:
· 摆脱Future.get()的阻塞
· 支持异步流水线(thenApply/thenAccept)
· 支持多任务组合(allOf/anyOf)
· 支持异常处理(exceptionally)
使用示例:
java
// 异步执行
CompletableFuture.supplyAsync(() -> {
return "hello";
}, threadPool)
.thenApplyAsync(result -> result + " world", threadPool)
.thenAcceptAsync(System.out::println, threadPool)
.exceptionally(e -> {
log.error("异常", e);
return "default";
});
开发经验:
· P6必须熟练使用,能极大简化异步逻辑
· 默认使用ForkJoinPool.commonPool(),建议自定义线程池
· 避免在回调中继续提交到同一个池(可能死锁)
第67题:CompletableFuture常用方法?
核心回答:
创建:
· supplyAsync():有返回值的异步任务
· runAsync():无返回值的异步任务
串行回调(有依赖):
· thenApply():转换结果(同步)
· thenApplyAsync():转换结果(异步)
· thenAccept():消费结果(无返回值)
· thenRun():执行完继续执行(不关心结果)
组合(无依赖):
· thenCombine():两个任务都完成,合并结果
· thenCompose():一个任务完成,用结果执行另一个
聚合:
· allOf():所有任务都完成
· anyOf():任意一个任务完成
异常:
· exceptionally():异常时兜底
· handle():无论异常还是正常都处理
· whenComplete():完成时回调
开发经验:
· 实际项目大量使用,要熟练掌握
· 示例:商品详情页聚合(商品信息、价格、库存、评价并发查询)
第68题:线程池异常处理?
核心回答:
四种处理方式:
方式1:try-catch(最常用)
java
executor.execute(() -> {
try {
// 业务逻辑
} catch (Exception e) {
log.error("任务执行异常", e);
// 降级处理
}
});
方式2:UncaughtExceptionHandler
java
ThreadFactory factory = r -> {
Thread t = new Thread(r);
t.setUncaughtExceptionHandler((thread, e) -> {
log.error("线程{}异常", thread.getName(), e);
});
return t;
};
方式3:Future.get()捕获
java
Future<?> future = executor.submit(() -> { throw new RuntimeException(); });
try {
future.get();
} catch (ExecutionException e) {
log.error("任务异常", e.getCause());
}
方式4:重写afterExecute()
java
protected void afterExecute(Runnable r, Throwable t) {
if (t != null) {
log.error("任务执行异常", t);
}
}
开发经验:
· execute提交:异常会抛到控制台,最好加try-catch
· submit提交:异常被封装,必须调用future.get()才能获取
· 生产环境绝对不能让异常静默,必须记录日志+告警
第69题:工作窃取算法?
核心回答:
定义: 每个线程都有自己的双端队列(Deque),当自己的队列为空时,从其他线程的队列尾部偷取任务执行。
优点:
· 充分利用CPU资源
· 减少线程空闲时间
· 降低竞争(从尾部取,本线程从头部取)
图示:
线程1: [A][B][C] → 从头部取任务执行
线程2: [D][E][F] → 队列空,从线程1尾部偷取[C]
应用: ForkJoinPool、ForkJoinTask
开发经验:
· 适用于分治任务(大任务拆小任务),任务间无依赖
· 任务粒度要适中,太小导致调度开销,太大无法平衡负载
· 实际项目中使用parallelStream()底层就是ForkJoinPool
第70题:ForkJoinPool?
核心回答:
定义: ForkJoinPool是专门用于分治任务的线程池,采用工作窃取算法。
核心组件:
· ForkJoinPool:线程池
· ForkJoinTask:任务(RecursiveAction无返回值,RecursiveTask有返回值)
· 工作窃取:空闲线程偷取其他队列任务
使用示例:
java
class SumTask extends RecursiveTask<Long> {
private final long[] array;
private final int start, end;
private static final int THRESHOLD = 10000;
@Override
protected Long compute() {
if (end - start <= THRESHOLD) {
// 直接计算
long sum = 0;
for (int i = start; i < end; i++) sum += array[i];
return sum;
}
int mid = (start + end) / 2;
SumTask left = new SumTask(array, start, mid);
SumTask right = new SumTask(array, mid, end);
left.fork(); // 异步执行
return right.compute() + left.join();
}
}
// 使用
ForkJoinPool pool = new ForkJoinPool(Runtime.getRuntime().availableProcessors());
Long result = pool.invoke(new SumTask(array, 0, array.length));
开发经验:
· 任务分解粒度不能太小(THRESHOLD要合理),否则调度开销大
· 适合大数据量并行计算(如批量数据处理、排序)
· 注意:ForkJoinPool的线程是守护线程,main结束就退出
五、高级特性篇(71-85)
第71题:volatile如何保证可见性?
核心回答:
可见性原理:
· volatile变量被修改时,JVM会向CPU发送lock前缀指令
· lock指令会锁定缓存行,强制将当前处理器的缓存行写回主内存
· 同时使其他CPU的缓存行失效(通过缓存一致性协议MESI)
底层流程:
线程1修改volatile变量
↓
JVM生成lock前缀指令
↓
CPU将修改后的值写回主内存
↓
总线嗅探机制通知其他CPU
↓
其他CPU的缓存行被标记为invalid
↓
线程2读取时,缓存行失效,从主内存重新加载
缓存一致性协议(MESI):
状态 说明
M(Modified) 已修改,只在本缓存中
E(Exclusive) 独占,与主内存一致
S(Shared) 共享,多缓存一致
I(Invalid) 无效,需重新加载
开发经验:
· volatile保证可见性但不保证原子性
· volatile int count; count++ 不是线程安全的(读-改-写三步骤)
· 理解MESI协议是P6加分项
第72题:volatile如何禁止指令重排?
核心回答:
指令重排: 编译器和CPU为了优化性能,会改变指令执行顺序,但保证单线程下结果不变。
volatile禁止重排原理: 插入内存屏障(Memory Barrier)
四种内存屏障:
屏障类型 作用
LoadLoad Load1; LoadLoad; Load2 → Load1先于Load2
StoreStore Store1; StoreStore; Store2 → Store1先于Store2
LoadStore Load1; LoadStore; Store2 → Load1先于Store2
StoreLoad Store1; StoreLoad; Load2 → Store1先于Load2(开销最大)
volatile的屏障策略:
volatile写:
[StoreStore屏障] → 普通写操作先行
volatile写操作
[StoreLoad屏障] → 确保写完成后才读
volatile读:
普通读操作
[LoadLoad屏障] → 确保volatile读后,后续读可见
[LoadStore屏障] → 确保volatile读后,后续写可见
volatile读操作
开发经验:
· 内存屏障是CPU指令,不是Java代码
· DCL单例中,volatile防止new对象的指令重排(分配内存→初始化→引用指向,可能重排为分配内存→引用指向→初始化,导致其他线程读到未初始化对象)
第73题:DCL单例为什么要加volatile?
核心回答:
DCL(Double Check Lock)单例代码:
java
public class Singleton {
private static volatile Singleton instance; // volatile必须
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton(); // ⚠️ 问题在这
}
}
}
return instance;
}
}
不加volatile的问题:
new Singleton() 在JVM中分三步:
- 分配内存空间
- 初始化对象
- 将引用指向内存地址
指令重排后可能变成: 1 → 3 → 2
· 线程A执行到步骤3,instance已指向内存地址,但对象还未初始化
· 线程B执行第一次检查 instance == null,发现不为null,直接返回
· 线程B使用未初始化的对象 → 程序崩溃
volatile禁止重排,保证2一定在3之前执行
开发经验:
· DCL单例是面试必考题,能讲清楚volatile的作用说明对JMM有深入理解
· 实际项目中,更推荐用静态内部类或枚举实现单例,更简洁
第74题:happens-before与volatile关系?
核心回答:
volatile规则: 对一个volatile变量的写操作 happens-before 对该变量的读操作。
示例:
java
volatile int a = 0;
int b = 0;
// 线程A
b = 1; // 操作1
a = 1; // 操作2(volatile写)
// 线程B
if (a == 1) { // 操作3(volatile读)
// 此时b一定等于1吗?✅ 一定等于1
System.out.println(b);
}
为什么?
· 操作1 happens-before 操作2(程序次序规则)
· 操作2 happens-before 操作3(volatile规则)
· 通过传递性:操作1 happens-before 操作3
· 所以操作1的结果对操作3可见,b一定等于1
开发经验:
· 这是JMM的核心规则,理解后能推断多线程程序的可见性
· 面试时能用这个规则推导可见性,是加分项
第75题:什么是内存屏障?
核心回答:
定义: 内存屏障(Memory Barrier/Fence)是一条CPU指令,用于强制保证内存操作顺序,禁止指令重排。
CPU级别的屏障(x86架构):
指令 含义
sfence 写屏障,保证之前的所有写操作完成
lfence 读屏障,保证之前的所有读操作完成
mfence 全屏障,读写都保证
JVM层面的屏障映射:
· StoreStore → sfence
· LoadLoad → lfence
· StoreLoad → mfence(开销最大)
volatile与内存屏障:
java
volatile int a;
a = 1; // 写:StoreStore + StoreLoad
int b = a; // 读:LoadLoad + LoadStore
开发经验:
· 理解内存屏障能帮你理解volatile和synchronized的底层
· 实际开发中不直接操作内存屏障
· 面试中能说出sfence/lfence/mfence是加分项
第76题:什么是AQS?
核心回答:
AQS(AbstractQueuedSynchronizer) 是Java并发包的同步器框架,ReentrantLock、Semaphore、CountDownLatch、CyclicBarrier等底层都是基于AQS。
AQS核心结构:
java
public abstract class AbstractQueuedSynchronizer {
// 同步状态(volatile)
private volatile int state;
// CLH队列头尾(双向链表)
private transient volatile Node head;
private transient volatile Node tail;
// 独占模式
protected boolean tryAcquire(int arg);
protected boolean tryRelease(int arg);
// 共享模式
protected int tryAcquireShared(int arg);
protected boolean tryReleaseShared(int arg);
}
核心设计: 模板方法模式
· 子类重写tryAcquire/tryRelease等方法
· 父类提供acquire/release等骨架方法
开发经验:
· 理解AQS是理解JUC包的基础
· 面试官经常问:"ReentrantLock的lock()是如何实现的?" → 本质是AQS的acquire()
第77题:AQS核心原理?
核心回答:
核心思想: 如果资源空闲,直接获取;如果资源被占用,将当前线程封装为Node,加入CLH队列,阻塞等待。
独占模式获取锁流程(acquire):
java
public final void acquire(int arg) {
// 1. 尝试获取锁(子类实现)
if (!tryAcquire(arg) &&
// 2. 失败则加入队列并阻塞
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 3. 响应中断
selfInterrupt();
}
private Node addWaiter(Node mode) {
// 创建Node,CAS加入队列尾部
}
final boolean acquireQueued(Node node, int arg) {
// 死循环:当前节点是头节点时尝试获取锁
// 否则阻塞(park)
}
释放锁流程(release):
java
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); // 唤醒后继节点
return true;
}
return false;
}
开发经验:
· 能画出AQS的流程图是P6面试的硬指标
· 理解acquire队列中节点的自旋+park机制
· 共享模式(Semaphore)和独占模式(ReentrantLock)的区别在tryAcquireShared
第78题:AQS中state的作用?
核心回答:
state是AQS的同步状态,用volatile修饰,语义由子类定义。
不同同步器的state含义:
同步器 state含义
ReentrantLock 0表示无锁,>0表示锁被占用(可重入次数)
Semaphore 剩余许可证数量
CountDownLatch 剩余需要countDown的次数
ReentrantReadWriteLock 高16位读锁计数,低16位写锁计数
ReentrantLock示例:
java
// tryAcquire
protected boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 无锁,尝试CAS获取
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) {
// 重入,state++
int nextc = c + acquires;
setState(nextc);
return true;
}
return false;
}
// tryRelease
protected boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) { // state归零才完全释放
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
开发经验:
· state是volatile,保证了可见性
· CAS更新state保证了原子性
· state的设计体现了AQS的灵活性
第79题:CLH队列是什么?
核心回答:
CLH队列(Craig, Landin, and Hagersten queue) 是AQS中管理等待线程的双向链表。
Node节点结构:
java
static final class Node {
volatile int waitStatus; // 等待状态
volatile Node prev; // 前驱节点
volatile Node next; // 后继节点
volatile Thread thread; // 当前线程
Node nextWaiter; // 等待Condition的节点
}
waitStatus取值:
值 含义
0 初始状态
SIGNAL(-1) 后继节点需要被唤醒
CONDITION(-2) 在Condition队列中等待
PROPAGATE(-3) 共享模式下传播
CANCELLED(1) 已取消
入队流程:
1. 创建Node节点
2. CAS设置tail为新节点
3. 设置新节点的prev为旧tail
4. 旧tail的next指向新节点
开发经验:
· CLH队列是AQS的核心,要能画出结构图
· 入队使用CAS保证线程安全,出队由头节点操作
· 理解Node的waitStatus对理解锁机制很重要
第80题:独占锁和共享锁区别?
核心回答:
对比 独占锁(Exclusive) 共享锁(Shared)
同时持有 只有一个线程 多个线程可同时持有
获取方法 acquire() acquireShared()
释放方法 release() releaseShared()
try方法 tryAcquire() tryAcquireShared()
典型实现 ReentrantLock Semaphore、CountDownLatch
核心区别在tryAcquireShared的返回值:
java
// 独占:返回boolean
protected boolean tryAcquire(int arg) {
// true表示获取成功
}
// 共享:返回int
protected int tryAcquireShared(int arg) {
// <0:失败
// 0:获取成功,但无需唤醒后续节点
// >0:获取成功,且需要唤醒后续节点(传播)
}
共享模式的传播(PROPAGATE):
· 当共享锁释放时,需要唤醒队列中的后继节点
· 后继节点获取锁后,继续唤醒下一个,形成链式唤醒
开发经验:
· 理解共享模式的传播机制是区分P6和P5的关键
· CountDownLatch的await就是共享锁的获取
第81题:什么是线程的等待/通知机制?
核心回答:
wait/notify机制:
· 基于Object类的方法,必须在synchronized块中调用
· wait():释放锁,进入等待集(WaitSet),阻塞
· notify():唤醒等待集中的一个线程(随机)
· notifyAll():唤醒等待集中的所有线程
使用示例:
java
synchronized (lock) {
while (condition) { // 必须用while,防止虚假唤醒
lock.wait();
}
// 执行业务
lock.notifyAll();
}
与Condition对比:
对比 wait/notify Condition
锁依赖 synchronized ReentrantLock
唤醒精度 notify随机 signal精确指定
多个等待集 ❌ 只有一个 ✅ 多个Condition
可中断 wait可中断 await可中断/不可中断
开发经验:
· wait必须放在while循环中,防止虚假唤醒
· 实际项目用BlockingQueue替代手写wait/notify
· 如需手写,优先用Condition,更灵活
第82题:wait和sleep区别?
核心回答:
对比 wait() sleep()
所属 Object类 Thread类
锁释放 ✅ 释放锁 ❌ 不释放锁
前提 必须在synchronized中 无要求
唤醒 notify/notifyAll 超时或interrupt
用途 线程间通信 暂停执行
示例:
java
// wait:释放锁,等待通知
synchronized (lock) {
lock.wait(); // 释放lock,进入等待
}
// sleep:不释放锁,单纯暂停
synchronized (lock) {
Thread.sleep(1000); // 持有lock,其他线程无法进入
}
开发经验:
· 这是面试必考题,必须能脱口而出
· wait用于生产者消费者,sleep用于模拟耗时或定时任务
· 两者都会响应中断,抛出InterruptedException
第83题:什么是Thread.join()?
核心回答:
作用: 等待调用join()的线程执行完毕,再继续执行当前线程。
示例:
java
Thread t = new Thread(() -> {
Thread.sleep(2000);
});
t.start();
t.join(); // 主线程阻塞,等待t执行完
System.out.println("t执行完成");
底层原理:
java
public final synchronized void join(long millis) throws InterruptedException {
// 如果线程还在存活,调用wait(millis)
while (isAlive()) {
wait(0); // 调用Object.wait()
}
}
// 线程执行完毕后,JVM会自动调用notifyAll()
开发经验:
· join本质是wait,释放锁(join是synchronized的)
· 适合多线程结果汇总的场景
· 实际项目用CountDownLatch或CompletableFuture替代join,更灵活
第84题:Thread.yield()?
核心回答:
作用: 让当前线程从运行状态变为就绪状态,让出CPU时间片给同优先级线程。
注意:
· 不释放锁
· 不保证生效(调度器可能无视)
· 让出的时间片可能又被当前线程获得
使用场景:
java
while (!condition) {
Thread.yield(); // 忙等待时让出CPU
}
底层原理:
· 调用native方法,最终由OS调度器决定
· Linux中yield会让线程移到队列末尾
开发经验:
· 生产环境几乎不用,调试或测试偶尔使用
· 不要依赖yield做业务逻辑,不可控
· 用Thread.sleep(0)也可以达到类似效果
第85题:什么是线程中断?
核心回答:
三个中断方法:
java
// 中断线程(设置中断标志位)
public void interrupt();
// 检查当前线程是否被中断(会清除中断标志位)
public static boolean interrupted();
// 检查线程是否被中断(不清除标志位)
public boolean isInterrupted();
中断状态:
· 调用interrupt()设置中断标志为true
· 线程在wait/sleep/join时被中断,会抛出InterruptedException,并清除中断标志
· 正常运行的线程,仅设置标志位,需手动检测
正确处理中断:
java
// ❌ 错误:吞掉中断
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// 什么都不做 → 中断丢失
}
// ✅ 正确:恢复中断
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 重新设置中断标志
// 或抛出
throw new RuntimeException(e);
}
// ✅ 正确:检测中断
while (!Thread.currentThread().isInterrupted()) {
// 执行任务
}
开发经验:
· 线程池的shutdownNow就是调用interrupt()
· 不能吞掉InterruptedException,必须恢复或抛出
· 自定义任务要响应中断,支持优雅停机
六、实战与设计篇(86-100)
第86题:生产者消费者实现方式?
核心回答:
三种实现方式:
方式一:wait/notify(最原始)
java
class Buffer {
private final Queue<Integer> queue = new LinkedList<>();
private final int MAX = 10;
public synchronized void put(int data) throws InterruptedException {
while (queue.size() == MAX) {
wait(); // 满了,等待
}
queue.offer(data);
notifyAll(); // 唤醒消费者
}
public synchronized int take() throws InterruptedException {
while (queue.isEmpty()) {
wait(); // 空了,等待
}
int data = queue.poll();
notifyAll(); // 唤醒生产者
return data;
}
}
方式二:ReentrantLock + Condition(更灵活)
java
class Buffer {
private final Queue<Integer> queue = new LinkedList<>();
private final int MAX = 10;
private final Lock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
public void put(int data) throws InterruptedException {
lock.lock();
try {
while (queue.size() == MAX) {
notFull.await(); // 等待不满
}
queue.offer(data);
notEmpty.signal(); // 唤醒消费者
} finally {
lock.unlock();
}
}
public int take() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) {
notEmpty.await(); // 等待不空
}
int data = queue.poll();
notFull.signal(); // 唤醒生产者
return data;
} finally {
lock.unlock();
}
}
}
方式三:BlockingQueue(生产环境推荐)
java
class Buffer {
private final BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);
public void put(int data) throws InterruptedException {
queue.put(data); // 满了阻塞
}
public int take() throws InterruptedException {
return queue.take(); // 空了阻塞
}
}
开发经验:
· 生产环境用BlockingQueue,代码最简洁,性能最好
· 手写wait/notify要注意:必须用while(防虚假唤醒),必须notifyAll(防死锁)
· Condition比wait/notify更安全(可以精确唤醒),推荐用于复杂场景
第87题:如何实现一个简单的限流器?
核心回答:
方式一:Semaphore(信号量限流)
java
public class SemaphoreLimiter {
private final Semaphore semaphore;
public SemaphoreLimiter(int maxPermits) {
this.semaphore = new Semaphore(maxPermits);
}
public void execute(Runnable task) throws InterruptedException {
semaphore.acquire(); // 获取许可
try {
task.run();
} finally {
semaphore.release(); // 释放许可
}
}
}
// 使用:同时最多10个线程执行
方式二:RateLimiter(令牌桶,Guava)
java
// 每秒生成2个令牌
RateLimiter limiter = RateLimiter.create(2.0);
public void execute(Runnable task) {
limiter.acquire(); // 获取令牌,阻塞等待
task.run();
}
// 或尝试获取,不阻塞
if (limiter.tryAcquire(1, TimeUnit.SECONDS)) {
task.run();
} else {
// 降级处理
}
方式三:漏桶算法(自实现)
java
public class LeakyBucketLimiter {
private final long capacity; // 桶容量
private final long rate; // 漏水速率(毫秒/滴)
private long water = 0; // 当前水量
private long lastLeakTime = System.currentTimeMillis();
public synchronized boolean tryAcquire() {
long now = System.currentTimeMillis();
// 先漏水
long leaked = (now - lastLeakTime) / rate;
water = Math.max(0, water - leaked);
lastLeakTime = now;
// 判断是否超过容量
if (water < capacity) {
water++;
return true;
}
return false;
}
}
开发经验:
· Semaphore适合控制并发数(如数据库连接池)
· RateLimiter适合控制QPS(如接口限流),Guava实现最常用
· 分布式限流用Redis + Lua脚本,保证原子性
第88题:如何实现分布式锁?
核心回答:
方式一:Redis分布式锁
java
public class RedisLock {
private final StringRedisTemplate redis;
private final String lockKey;
private final String lockValue; // 唯一ID,防止误删
public boolean tryLock(long timeout, TimeUnit unit) {
String script = "if redis.call('setnx', KEYS[1], ARGV[1]) == 1 " +
"then redis.call('expire', KEYS[1], ARGV[2]) return 1 " +
"else return 0 end";
// SETNX + EXPIRE 原子操作
Boolean result = redis.execute(
new DefaultRedisScript<>(script, Boolean.class),
Arrays.asList(lockKey),
lockValue, String.valueOf(timeout)
);
return Boolean.TRUE.equals(result);
}
public void unlock() {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] " +
"then return redis.call('del', KEYS[1]) " +
"else return 0 end";
// 只有持有锁的线程才能删除
redis.execute(
new DefaultRedisScript<>(script, Long.class),
Arrays.asList(lockKey),
lockValue
);
}
}
方式二:ZooKeeper分布式锁
java
// 使用Curator框架
InterProcessMutex lock = new InterProcessMutex(client, "/lock/path");
if (lock.acquire(10, TimeUnit.SECONDS)) {
try {
// 业务逻辑
} finally {
lock.release();
}
}
开发经验:
· Redis锁:性能高,但注意锁续期问题(看门狗机制)
· ZK锁:可靠性高,性能较低,适合对一致性要求高的场景
· Redis锁的RedLock算法(多个Redis节点)是更可靠的方案
· 锁的value必须唯一(用UUID),防止误删其他线程的锁
第89题:如何避免缓存雪崩?
核心回答:
定义: 大量缓存Key在同一时间过期,导致请求全部打到数据库,压垮DB。
解决方案:
方案一:过期时间加随机值
java
// 原:所有key 1小时过期
// 改为:1小时 ± 随机分钟
int baseExpire = 3600;
int randomOffset = new Random().nextInt(300); // 0-5分钟
int expire = baseExpire + randomOffset;
redis.setex(key, expire, value);
方案二:多级缓存
本地缓存(Caffeine) → Redis缓存 → 数据库
· 本地缓存命中率高,减轻Redis压力
方案三:互斥锁重建缓存
java
public Object getData(String key) {
Object value = redis.get(key);
if (value != null) return value;
// 加互斥锁,只有一个线程去查DB
String lockKey = "lock:" + key;
if (redis.setnx(lockKey, "1", 10, TimeUnit.SECONDS)) {
try {
value = db.query(key);
redis.setex(key, 3600, value);
} finally {
redis.delete(lockKey);
}
} else {
// 等待一小段时间,再尝试获取缓存
Thread.sleep(50);
return getData(key);
}
return value;
}
方案四:缓存永不过期 + 后台异步刷新
java
// 缓存不设过期时间,后台线程定时刷新
@Scheduled(fixedDelay = 60000)
public void refreshCache() {
// 刷新热点数据
}
开发经验:
· 实际项目组合使用方案一(过期时间分散)+ 方案二(多级缓存)
· 互斥锁方案要注意死锁和超时问题
· 缓存雪崩是生产事故高发原因,必须重视
第90题:如何避免缓存击穿?
核心回答:
定义: 某个热点Key过期瞬间,大量请求同时访问该Key,全部打到DB。
与雪崩区别: 雪崩是大量Key同时过期,击穿是单个热点Key过期。
解决方案:
方案一:互斥锁(与雪崩类似,但粒度更细)
java
public Object getHotData(String key) {
Object value = redis.get(key);
if (value != null) return value;
// 加锁,只让一个线程去查DB
String lockKey = "lock:" + key;
if (redis.setnx(lockKey, "1", 10, TimeUnit.SECONDS)) {
try {
value = db.query(key);
redis.setex(key, 3600, value);
} finally {
redis.delete(lockKey);
}
} else {
Thread.sleep(50);
return getHotData(key); // 递归重试
}
return value;
}
方案二:逻辑过期(永不过期 + 异步更新)
java
// 存储:value + 逻辑过期时间
class CacheData {
Object data;
long expireTime;
}
public Object getHotData(String key) {
CacheData cache = redis.get(key);
if (cache == null) return db.query(key);
if (cache.expireTime < System.currentTimeMillis()) {
// 逻辑过期,异步更新
asyncRefresh(key);
}
return cache.data; // 返回旧数据,不阻塞
}
@Async
public void asyncRefresh(String key) {
// 加锁,只让一个线程更新
if (redis.setnx("lock:" + key, "1", 5, TimeUnit.SECONDS)) {
try {
Object newData = db.query(key);
CacheData newCache = new CacheData(newData, now + 3600);
redis.set(key, newCache);
} finally {
redis.delete("lock:" + key);
}
}
}
开发经验:
· 互斥锁:实现简单,但会阻塞请求(适合一致性要求高的场景)
· 逻辑过期:不阻塞,返回旧数据,用户体验好(适合一致性要求不高的场景)
· 热点Key要提前识别,做好预案
第91题:如何避免缓存穿透?
核心回答:
定义: 查询不存在的数据,请求绕过缓存直接打DB。恶意攻击可能用大量不存在的Key压垮DB。
解决方案:
方案一:布隆过滤器(Bloom Filter)
java
// 初始化:将存在的Key加入布隆过滤器
BloomFilter<String> filter = BloomFilter.create(
Funnels.stringFunnel(Charsets.UTF_8),
1000000, // 预期插入量
0.001 // 误判率
);
// 项目启动时,加载所有合法Key
db.queryAllKeys().forEach(filter::put);
// 查询时
public Object getData(String key) {
if (!filter.mightContain(key)) {
return null; // 一定不存在,直接返回
}
// 可能存在(有小概率误判),查缓存→查DB
return getFromCacheOrDB(key);
}
方案二:缓存空值
java
public Object getData(String key) {
Object value = redis.get(key);
if (value != null) {
// 判断是否为"空值标记"
if (value instanceof NullValue) {
return null; // 曾查过,不存在
}
return value;
}
value = db.query(key);
if (value == null) {
// 缓存空值,过期时间设置短一些
redis.setex(key, 60, new NullValue());
return null;
}
redis.setex(key, 3600, value);
return value;
}
方案三:参数校验
java
public Object getData(String key) {
// 校验参数格式(如ID不能为负、不能超长)
if (!isValid(key)) {
return null;
}
// 正常查询
}
开发经验:
· 布隆过滤器是最彻底的方案,但需要维护数据同步
· 缓存空值实现简单,适合临时方案,注意空值过期时间要短
· 三种方案组合使用效果最好
第92题:线程安全问题排查思路?
核心回答:
排查流程:
步骤1:确认问题现象
· 数据不一致(金额不对、订单状态错乱)
· 死锁(业务卡住)
· CPU飙升(死循环、频繁GC)
· 响应变慢(锁竞争)
步骤2:收集现场信息
bash
# 查看Java进程
jps -l
# 导出线程栈(核心)
jstack <pid> > thread.log
# 查看GC情况
jstat -gcutil <pid> 1000
# 查看内存使用
jmap -heap <pid>
# 线上用arthas更便捷
arthas attach <pid>
thread -n 10 # 查看最忙的10个线程
步骤3:分析线程栈
· 死锁:搜索"deadlock",会直接显示
· BLOCKED状态的线程:查看在等待什么锁
· WAITING状态的线程:查看在等待什么条件
步骤4:定位代码
· 根据栈信息找到具体的类和方法
· 检查是否有共享变量未同步
· 检查锁的获取顺序(死锁)
步骤5:修复验证
· 修复代码,压测验证
· 灰度发布,观察监控
常用工具:
工具 用途
jstack 查看线程栈
jmap 查看堆内存
jstat 查看GC
arthas 在线诊断神器
VisualVM 可视化监控
开发经验:
· 线上问题先导出jstack,这是第一手资料
· 死锁问题jstack直接能看出来,非常清晰
· CPU飙升:先看GC是否频繁(jstat),再看是否有死循环(top -H -p pid看哪个线程CPU高,再查jstack)
第93题:高并发系统常见性能瓶颈?
核心回答:
六大瓶颈:
- 数据库连接池
java
// 问题:连接池太小,等待获取连接成为瓶颈
HikariCP: maximumPoolSize = 10 // 太小
// 解决:根据压测调整,一般20-50
- 网络IO
· 同步IO阻塞线程,导致线程资源浪费
· 解决:使用异步IO(Netty)、NIO
- 锁竞争
· 使用synchronized/ReentrantLock导致线程阻塞
· 解决:减少锁粒度(ConcurrentHashMap)、无锁化(CAS)
- GC停顿
· 频繁Full GC导致STW(Stop The World)
· 解决:减少对象创建、调整GC参数(G1/ZGC)、增大堆内存
- 缓存未命中
· 缓存命中率低,大量请求打DB
· 解决:增加缓存容量、预加载热点数据
- 线程上下文切换
· 线程数过多,CPU大部分时间花在切换上
· 解决:合理设置线程池大小
开发经验:
· 压测是定位瓶颈的唯一可靠方法
· 用火焰图(async-profiler)定位CPU热点
· 优化原则:先优化数据库,再优化代码,最后优化架构
第94题:如何优化锁性能?
核心回答:
六大优化策略:
- 减小锁粒度
java
// ❌ 粗粒度:锁整个Map
synchronized(map) { map.put(key, value); }
// ✅ 细粒度:只锁一个桶(ConcurrentHashMap的做法)
synchronized(bucket) { bucket.put(key, value); }
- 读写分离
java
// ❌ 读写都用同一把锁
synchronized void read() {}
synchronized void write() {}
// ✅ 读用读锁,写用写锁
ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
rw.readLock().lock(); // 读
rw.writeLock().lock(); // 写
- 锁粗化(JVM自动优化)
· 将多个锁操作合并为一次
· 但手动写代码时也要注意
- 减少锁持有时间
java
// ❌ 锁住整个方法
synchronized void process() {
// 耗时计算 1秒
// 更新状态 1ms
}
// ✅ 只锁必要的代码
void process() {
// 耗时计算 1秒(无锁)
synchronized(this) {
// 更新状态 1ms
}
}
- 使用无锁方案(CAS)
java
// ❌ 有锁
AtomicInteger count = new AtomicInteger(0);
synchronized { count.incrementAndGet(); }
// ✅ 无锁(CAS)
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // CAS自旋
- 使用LongAdder替代AtomicLong(写多读少)
java
// 高并发写场景
LongAdder counter = new LongAdder();
counter.increment(); // 性能远高于AtomicLong
开发经验:
· 优化顺序:无锁CAS → 读写锁 → 减小锁粒度 → 减少锁持有时间
· 实际项目中最常用的是ConcurrentHashMap(减小锁粒度)和LongAdder(无锁)
第95题:什么是无锁编程?
核心回答:
定义: 不使用互斥锁(synchronized/ReentrantLock),通过CAS(Compare And Swap)实现线程安全。
无锁编程的核心理念:
· 乐观锁:假设没有冲突,失败则重试
· 硬件支持:CPU的CAS指令保证原子性
Java中的无锁实现:
java
// Atomic系列
AtomicInteger ai = new AtomicInteger(0);
ai.incrementAndGet(); // CAS + 自旋
// 无锁队列
ConcurrentLinkedQueue<E> queue = new ConcurrentLinkedQueue<>();
queue.offer(item); // CAS
// 无锁Map
ConcurrentHashMap<K,V> map = new ConcurrentHashMap<>();
map.put(key, value); // CAS + synchronized(桶)
自定义CAS操作:
java
public class LockFreeCounter {
private final AtomicReference<State> state = new AtomicReference<>(new State(0));
public void increment() {
State oldState, newState;
do {
oldState = state.get();
newState = new State(oldState.value + 1);
} while (!state.compareAndSet(oldState, newState));
// 自旋直到成功
}
}
优缺点:
优点 缺点
无上下文切换 高竞争下自旋耗CPU
无死锁 ABA问题
高性能 只能保护单一变量
开发经验:
· 无锁编程是高性能系统的核心手段
· 但不要过度使用,CAS自旋在高竞争下性能急剧下降
· LongAdder的设计思路:分散竞争,是更好的实践
第96题:什么是偏向锁的批量重偏向?
核心回答:
背景:
· 偏向锁适用于只有一个线程访问同步块的场景
· 当发生竞争时,偏向锁撤销(revoke)升级为轻量级锁
· 撤销操作有开销(需要等待全局安全点)
批量重偏向(Bulk Rebias):
· 当某个类的偏向锁撤销次数达到阈值(默认20次)时
· JVM认为这个类处于"多线程交替执行"状态
· 执行批量重偏向:允许对象重新偏向到新线程
批量撤销(Bulk Revoke):
· 当撤销次数达到第二个阈值(默认40次)时
· JVM认为这个类竞争激烈,批量撤销所有偏向锁
· 该类后续所有对象使用轻量级锁
参数:
bash
-XX:BiasedLockingBulkRebiasThreshold=20 # 批量重偏向阈值
-XX:BiasedLockingBulkRevokeThreshold=40 # 批量撤销阈值
开发经验:
· 这是JVM层面的优化,写代码不需要关心
· 面试能说出这两个概念,说明对JVM锁优化有深入研究
· JDK15已默认禁用偏向锁(维护成本大于收益)
第97题:如何设计一个高并发秒杀系统?
核心回答:
架构图:
用户请求 → 页面静态化 → 限流(Nginx) → 网关 →
↓
业务逻辑:Redis预扣库存 → MQ异步 → 数据库扣库存 → 订单落库
↓
返回结果
核心技术点:
- 页面静态化 + CDN
· 秒杀页面静态化,减少后端压力
· 静态资源放CDN,加速访问
- 限流
java
// Nginx限流:每秒最多1000请求
limit_req zone=seckill burst=100 nodelay;
// 业务层限流:Guava RateLimiter
RateLimiter limiter = RateLimiter.create(100);
if (!limiter.tryAcquire()) {
return "排队中";
}
- 缓存预扣库存(Redis)
java
// Lua脚本保证原子性
String script = "local stock = redis.call('get', KEYS[1]) " +
"if (stock and tonumber(stock) > 0) then " +
" redis.call('decr', KEYS[1]) " +
" return 1 " +
"else return 0 end";
// 预扣成功才允许下单
- 消息队列异步削峰
java
// 预扣成功后,发送消息异步创建订单
mq.send("order.create", orderData);
// 消费者:扣DB库存 + 创建订单
- 数据库乐观锁
sql
UPDATE product
SET stock = stock - 1
WHERE id = #{id} AND stock > 0;
-- 利用行锁,通过影响行数判断是否成功
- 防刷机制
· 限购:一个用户只能买一件
· 验证码:防止机器人刷
· IP限流:同一IP限制请求次数
开发经验:
· 库存扣减必须用Redis Lua,保证原子性
· 最终一致性:Redis预扣成功 → MQ → DB扣减(失败则补偿)
· 超时未支付要恢复库存(MQ延迟消息)
· 压测是验证秒杀系统的唯一标准
第98题:异步编程和同步编程区别?
核心回答:
对比 同步 异步
阻塞 ✅ 阻塞等待结果 ❌ 不阻塞
线程资源 每个请求占用一个线程 少量线程处理大量请求
编程复杂度 简单 复杂(回调地狱)
吞吐量 低 高
适合场景 简单请求 IO密集型、高并发
同步示例:
java
// 串行执行,阻塞等待
User user = userService.getUser(id);
Order order = orderService.getOrder(user);
Coupon coupon = couponService.getCoupon(order);
// 总耗时 = 三者之和
异步示例(CompletableFuture):
java
// 并行执行,不阻塞
CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(() -> userService.getUser(id));
CompletableFuture<Order> orderFuture = userFuture.thenApplyAsync(user -> orderService.getOrder(user));
CompletableFuture<Coupon> couponFuture = orderFuture.thenApplyAsync(order -> couponService.getCoupon(order));
// 总耗时 ≈ 三者中最长的那个
开发经验:
· 异步编程能极大提升IO密集型系统的吞吐量
· Spring WebFlux + Netty是异步编程的典型框架
· 异步编程要注意线程池隔离,避免相互影响
第99题:响应式编程在Java中的实现?
核心回答:
响应式编程(Reactive Programming): 基于事件驱动和数据流的异步编程范式,支持背压(Backpressure)。
Java生态:
· Project Reactor:Spring WebFlux底层
· RxJava:Netflix开源
核心概念:
概念 说明
Publisher 数据发布者
Subscriber 数据订阅者
Subscription 订阅关系,用于背压控制
Processor 既是Publisher又是Subscriber
背压(Backpressure):
· 消费者处理速度 < 生产者生产速度时
· 消费者通知生产者"慢一点"
· 通过Subscription的request(n)实现
使用示例(Reactor):
java
// 创建Flux数据流
Flux<Integer> flux = Flux.range(1, 100)
.map(i -> i * i)
.filter(i -> i % 2 == 0)
.limitRate(10); // 背压:每次请求10个
// 订阅
flux.subscribe(
data -> System.out.println("onNext: " + data),
error -> System.err.println("onError: " + error),
() -> System.out.println("onComplete")
);
// WebFlux Controller
@GetMapping("/users")
public Flux<User> getUsers() {
return userService.findAll(); // 异步非阻塞
}
开发经验:
· 响应式编程适合IO密集型、高并发场景
· 学习曲线陡峭,调试困难
· 国内使用场景较少,但大厂在逐步推广
第100题:你遇到过最复杂的并发问题?
核心回答:
答题要点:
这是开放题,没有标准答案。面试官想听的是:
- 你是否有真实的线上经验
- 你的排查思路是否清晰
- 你的解决能力是否到位
参考回答模板:
我遇到过的一个复杂问题是【缓存击穿导致DB连接池爆满】。
现象:某个热点商品ID的缓存突然过期,大量请求同时打到DB,
数据库连接池瞬间被占满,其他正常业务也受影响。
排查过程:
1. 先看监控,发现DB连接数飙升,然后jstack看到大量线程
BLOCKED在获取数据库连接。
2. 查看日志,发现大量"商品详情"查询走DB,确认是缓存击穿。
3. 进一步看代码,发现缓存的过期时间设置不合理,
所有热点商品同时过期。
解决方案:
1. 紧急:手动在Redis中重建热点数据
2. 短期:为热点商品设置不同的过期时间(加随机偏移)
3. 长期:实现"逻辑过期"方案,缓存永不过期,
后台异步刷新,并加互斥锁防止重复查DB
结果:问题解决,后续没有再发生。
总结:并发问题一定从监控中发现,定位靠日志和jstack,
解决要兼顾短期和长期方案。
开发经验:
· 准备一个真实经历,越具体越好
· 体现排查思路(监控→jstack→日志→代码)
· 体现解决能力(紧急处理→临时方案→长期优化)