JAVA 并发编程

一、基础概念篇(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条规则(必背):

  1. 程序次序规则:同一线程中,代码顺序前面的happens-before后面的。
  2. 管程锁定规则:unlock happens-before lock(同一个锁)。
  3. volatile规则:volatile写 happens-before 之后的volatile读。
  4. 线程启动规则:start() happens-before 该线程的任何操作。
  5. 线程终止规则:线程所有操作 happens-before join()返回。
  6. 线程中断规则:interrupt() happens-before 检测到中断。
  7. 对象终结规则:构造方法执行完 happens-before finalize()。
  8. 传递性: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题:死锁四个必要条件?

核心回答:

死锁必须同时满足四个条件:

  1. 互斥条件:资源一次只能被一个线程占用。
  2. 占有并等待:线程持有资源,等待其他资源。
  3. 不可剥夺条件:资源只能由持有者释放,不能被抢占。
  4. 循环等待条件:线程间形成循环等待链。

示例:

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读取

核心机制:

  1. CAS:用于桶的初始化、插入第一个节点
  2. synchronized:锁住桶的头节点,只影响当前桶
  3. volatile:保证table、Node的可见性
  4. 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):

  1. 创建新table,容量为原来的2倍
  2. 多线程协同扩容,每个线程负责一部分旧桶
  3. 迁移时,旧桶节点被移动到新table
  4. 迁移完成后,旧桶标记为ForwardingNode(转发节点)
  5. 全部迁移完成,新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() - 抛异常

四套接口:

  1. 抛异常:add、remove、element
  2. 返回特殊值:offer、poll、peek
  3. 阻塞:put、take
  4. 超时: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: 存储的值

核心原理:

  1. 每个线程维护一个ThreadLocalMap
  2. set(value):以当前ThreadLocal为key,存入当前线程的Map
  3. get():从当前线程的Map中获取值
  4. 不同线程访问同一个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题:为什么要用线程池?

核心回答:

三大好处:

  1. 降低资源消耗:复用已创建的线程,避免频繁创建/销毁的开销
  2. 提高响应速度:任务来了直接执行,不用等待创建线程
  3. 提高线程管理性:统一分配、调优、监控,控制并发数

对比:

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?
                              ↓ 是        ↓ 否
                          创建新线程执行  ④ 执行拒绝策略

文字版:

  1. 当前线程数 < corePoolSize → 创建新核心线程执行
  2. 当前线程数 ≥ corePoolSize → 任务放入workQueue
  3. workQueue已满 → 创建新线程执行(直到maximumPoolSize)
  4. 线程数 = 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创建?

核心回答:

三大隐患:

  1. FixedThreadPool / SingleThreadExecutor:

· 使用无界队列 LinkedBlockingQueue(容量Integer.MAX_VALUE)

· 任务堆积可能OOM

java 复制代码
// 如果任务速度 > 消费速度,内存持续增长
newFixedThreadPool(10).execute(() -> {
    while(true) { // 任务处理慢 }
});
// → 队列无限增长,OOM
  1. CachedThreadPool / ScheduledThreadPool:

· 最大线程数为Integer.MAX_VALUE

· 线程无限创建,可能导致OOM或CPU耗尽

  1. 不利于监控:

· 没有自定义线程名,排障困难

· 无法统一管理参数

开发经验:

· 阿里规范明确禁止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倍

实际调优方法:

  1. 通过压测确定最佳线程数
  2. 从核心数*2开始,逐步增加,观察TPS和响应时间
  3. 找到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(); // 等待子任务
    // 如果所有线程都在等待子任务,死锁
});

解决方案:

  1. 不同业务用不同线程池(任务隔离)
  2. 增加核心线程数,确保有线程处理子任务
  3. 避免任务提交到自己所在的线程池

开发经验:

· 异步编排时要注意任务依赖关系

· 使用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. 分配内存空间
  2. 初始化对象
  3. 将引用指向内存地址

指令重排后可能变成: 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题:高并发系统常见性能瓶颈?

核心回答:

六大瓶颈:

  1. 数据库连接池
java 复制代码
// 问题:连接池太小,等待获取连接成为瓶颈
HikariCP: maximumPoolSize = 10 // 太小
// 解决:根据压测调整,一般20-50
  1. 网络IO

· 同步IO阻塞线程,导致线程资源浪费

· 解决:使用异步IO(Netty)、NIO

  1. 锁竞争

· 使用synchronized/ReentrantLock导致线程阻塞

· 解决:减少锁粒度(ConcurrentHashMap)、无锁化(CAS)

  1. GC停顿

· 频繁Full GC导致STW(Stop The World)

· 解决:减少对象创建、调整GC参数(G1/ZGC)、增大堆内存

  1. 缓存未命中

· 缓存命中率低,大量请求打DB

· 解决:增加缓存容量、预加载热点数据

  1. 线程上下文切换

· 线程数过多,CPU大部分时间花在切换上

· 解决:合理设置线程池大小

开发经验:

· 压测是定位瓶颈的唯一可靠方法

· 用火焰图(async-profiler)定位CPU热点

· 优化原则:先优化数据库,再优化代码,最后优化架构

第94题:如何优化锁性能?

核心回答:

六大优化策略:

  1. 减小锁粒度
java 复制代码
// ❌ 粗粒度:锁整个Map
synchronized(map) { map.put(key, value); }

// ✅ 细粒度:只锁一个桶(ConcurrentHashMap的做法)
synchronized(bucket) { bucket.put(key, value); }
  1. 读写分离
java 复制代码
// ❌ 读写都用同一把锁
synchronized void read() {}
synchronized void write() {}

// ✅ 读用读锁,写用写锁
ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
rw.readLock().lock(); // 读
rw.writeLock().lock(); // 写
  1. 锁粗化(JVM自动优化)

· 将多个锁操作合并为一次

· 但手动写代码时也要注意

  1. 减少锁持有时间
java 复制代码
// ❌ 锁住整个方法
synchronized void process() {
    // 耗时计算 1秒
    // 更新状态 1ms
}

// ✅ 只锁必要的代码
void process() {
    // 耗时计算 1秒(无锁)
    synchronized(this) {
        // 更新状态 1ms
    }
}
  1. 使用无锁方案(CAS)
java 复制代码
// ❌ 有锁
AtomicInteger count = new AtomicInteger(0);
synchronized { count.incrementAndGet(); }

// ✅ 无锁(CAS)
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // CAS自旋
  1. 使用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异步 → 数据库扣库存 → 订单落库
    ↓
返回结果

核心技术点:

  1. 页面静态化 + CDN

· 秒杀页面静态化,减少后端压力

· 静态资源放CDN,加速访问

  1. 限流
java 复制代码
// Nginx限流:每秒最多1000请求
limit_req zone=seckill burst=100 nodelay;

// 业务层限流:Guava RateLimiter
RateLimiter limiter = RateLimiter.create(100);
if (!limiter.tryAcquire()) {
    return "排队中";
}
  1. 缓存预扣库存(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";
// 预扣成功才允许下单
  1. 消息队列异步削峰
java 复制代码
// 预扣成功后,发送消息异步创建订单
mq.send("order.create", orderData);
// 消费者:扣DB库存 + 创建订单
  1. 数据库乐观锁
sql 复制代码
UPDATE product 
SET stock = stock - 1 
WHERE id = #{id} AND stock > 0;
-- 利用行锁,通过影响行数判断是否成功
  1. 防刷机制

· 限购:一个用户只能买一件

· 验证码:防止机器人刷

· 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题:你遇到过最复杂的并发问题?

核心回答:

答题要点:

这是开放题,没有标准答案。面试官想听的是:

  1. 你是否有真实的线上经验
  2. 你的排查思路是否清晰
  3. 你的解决能力是否到位

参考回答模板:

复制代码
我遇到过的一个复杂问题是【缓存击穿导致DB连接池爆满】。

现象:某个热点商品ID的缓存突然过期,大量请求同时打到DB,
数据库连接池瞬间被占满,其他正常业务也受影响。

排查过程:
1. 先看监控,发现DB连接数飙升,然后jstack看到大量线程
   BLOCKED在获取数据库连接。
2. 查看日志,发现大量"商品详情"查询走DB,确认是缓存击穿。
3. 进一步看代码,发现缓存的过期时间设置不合理,
   所有热点商品同时过期。

解决方案:
1. 紧急:手动在Redis中重建热点数据
2. 短期:为热点商品设置不同的过期时间(加随机偏移)
3. 长期:实现"逻辑过期"方案,缓存永不过期,
   后台异步刷新,并加互斥锁防止重复查DB

结果:问题解决,后续没有再发生。
总结:并发问题一定从监控中发现,定位靠日志和jstack,
解决要兼顾短期和长期方案。

开发经验:

· 准备一个真实经历,越具体越好

· 体现排查思路(监控→jstack→日志→代码)

· 体现解决能力(紧急处理→临时方案→长期优化)


相关推荐
vHelios2 小时前
【电商项目】商品服务模块的问题解决与代码逻辑思考
java·sql·mybatis
rannn_1112 小时前
【力扣hot100】238、41、73题解
java·算法·leetcode·开发
今天的砖头有点烫手啊2 小时前
Spring Boot
java·人工智能
码农颜12 小时前
5.4.1 锁分类
java·数据库·mysql
linux-hzh13 小时前
百日算法修炼 · Day 03
java·算法
不负岁月无痕14 小时前
简单理解操作系统结构
java·linux·c语言·开发语言·c++·面试
mister_guo14 小时前
Java线程池参数应该如何设置(ThreadPoolExecutor)
java
余额瞒着我当琳14 小时前
C++模板初阶与STL初探
android·java·c++
淡海水14 小时前
15-02-YooAsset面试篇-Unity架构设计与源码
java·unity·面试·c#·游戏引擎·yooasset