Skip to content

JUC 并发编程 ​

从「为什么会有并发问题」讲到「线上线程池打满怎么查」。02 篇的多线程只讲到「怎么创建线程」,这篇补的是内存模型、锁、AQS、线程池源码、并发容器这一层。

1. 并发问题的三个根源 ​

所有并发 bug 都能归到三条里:

问题含义典型现象
原子性一个或多个操作要么全做、要么全不做i++ 丢更新、先查后改的竞态
可见性一个线程改了共享变量,另一个线程要能立刻看到死循环读不到 stop 标志位
有序性编译器 / CPU 的指令重排不能改变单线程语义,但会打乱多线程下的执行顺序半初始化对象被别的线程看到

为什么会有可见性问题:CPU 有自己的 L1/L2 缓存,写操作先落到缓存,靠缓存一致性协议(MESI)同步;同时为了压榨性能,写缓冲区和无效化队列会让「写入」和「其他核看到」之间存在延迟。Java 不去直接管硬件,而是抽象出一套 JMM(Java 内存模型) 来规定「什么时候必须可见」。

线程 A                主内存                线程 B
  |                     |                     |
工作内存 A  <--read---  共享变量 x=0  ---read-->  工作内存 B
  |                     |                     |
写 x=1 -> 先写本地缓存,何时刷回主内存?—— 由 JMM 的 happens-before 决定
线程 A                主内存                线程 B
  |                     |                     |
工作内存 A  <--read---  共享变量 x=0  ---read-->  工作内存 B
  |                     |                     |
写 x=1 -> 先写本地缓存,何时刷回主内存?—— 由 JMM 的 happens-before 决定

2. JMM 与 happens-before ​

2.1 happens-before 是什么 ​

JMM 用 happens-before 定义可见性:如果 A happens-before B,那么 A 的结果对 B 可见,且 A 在 B 之前执行(对程序员可见的顺序,不代表 CPU 真的按这个顺序跑)。

8 条规则(记住前 6 条够用):

  1. 程序顺序:单线程内,前面的操作 hb 后面的操作。
  2. 监视器锁:对同一个锁的 unlock hb 后续的 lock。
  3. volatile:对 volatile 变量的写 hb 后续对这个变量的读。
  4. 线程启动:Thread.start() hb 该线程里的任何操作。
  5. 线程终止:线程里的所有操作 hb 其他线程检测到它终止(join() 返回)。
  6. 中断:对线程 interrupt() hb 被中断线程检测到中断。
  7. 对象终结:构造函数结束 hb finalize() 开始。
  8. 传递性:A hb B、B hb C,则 A hb C。

2.2 as-if-serial 与指令重排 ​

单线程里,只要最终结果不变,编译器、JIT、CPU 都可以重排。所以这段代码里 a 和 b 的赋值顺序不保证:

java
int a = 1;   // 可能后执行
int b = 2;   // 可能先执行
int a = 1;   // 可能后执行
int b = 2;   // 可能先执行

但 x = 1; y = x + 1; 不会重排成先算 y —— 这就是 as-if-serial 的边界:有数据依赖的不能重排。

2.3 内存屏障 ​

JMM 把屏障分成四类,是 volatile 和锁的底层实现手段:

屏障作用
LoadLoad前面的读先于后面的读
StoreStore前面的写先于后面的写
LoadStore前面的读先于后面的写
StoreLoad前面的写先于后面的读(开销最大,volatile 写后必插)

x86 上只有 StoreLoad 需要显式指令(lock 前缀 / mfence),其余三种靠 x86 本身较强的内存模型天然满足 —— 所以「volatile 在 x86 上几乎无额外开销」这句话只对读成立。

3. volatile:只管可见性和有序性 ​

3.1 三个语义 ​

  1. 可见性:写 volatile 变量时,会把该线程工作内存中的值刷回主内存;读时会从主内存重新读。
  2. 禁止重排:写前插 StoreStore,写后插 StoreLoad;读后插 LoadLoad / LoadStore。
  3. 不保证原子性:volatile int i; i++ 依然是「读-改-写」三步,照样丢更新。

3.2 为什么 DCL 单例必须加 volatile ​

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();   // 1.分配内存 2.初始化 3.引用指向内存
                }
            }
        }
        return instance;
    }
}
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();   // 1.分配内存 2.初始化 3.引用指向内存
                }
            }
        }
        return instance;
    }
}

new 不是原子的。2 和 3 可能重排:别的线程看到 instance != null 就直接返回,但此时对象字段还是默认值。

3.3 什么时候用 volatile ​

  • 状态标志位(volatile boolean running)
  • 一次性安全发布(DCL 的 instance)
  • 读多写少、写不依赖当前值(比如配置项、计数器只用于统计可容忍误差)

不要用它做 i++、复合判断、需要互斥的临界区。

3.4 volatile 与 synchronized 的区别 ​

volatilesynchronized
作用范围变量代码块 / 方法
原子性不保证(单次读写除外,long/double 的 64 位读写靠它保证)保证
可见性 / 有序性保证保证
阻塞不阻塞会阻塞
底层内存屏障monitor(monitorenter / monitorexit)

4. synchronized 与锁升级 ​

4.1 字节码层面 ​

java
synchronized (lock) { ... }
synchronized (lock) { ... }

编译后是 monitorenter / monitorexit(正常退出一个、异常退出一个,所以必须成对)。方法是 ACC_SYNCHRONIZED 标志位。二者最终都落到 monitor 对象(HotSpot 里是 ObjectMonitor,有 _owner、_EntryList、_WaitSet、_recursions)。

4.2 锁状态存在哪:对象头 Mark Word ​

64 位 JVM 的 Mark Word(8 字节):

锁状态62 位内容
无锁unused:25 + hash:31 + unused:1 + age:4 + 0 + 01
偏向锁线程 ID:54 + epoch:2 + unused:1 + age:4 + 1 + 01
轻量级锁指向栈中锁记录的指针:62 + 00
重量级锁指向 ObjectMonitor 的指针:62 + 10
GC 标记空 + 11

4.3 升级路径 ​

无锁 --(第一个线程 CAS 记 ID)--> 偏向锁 --(第二个线程竞争)--> 轻量级锁 --(自旋失败/竞争加剧)--> 重量级锁
无锁 --(第一个线程 CAS 记 ID)--> 偏向锁 --(第二个线程竞争)--> 轻量级锁 --(自旋失败/竞争加剧)--> 重量级锁
  • 偏向锁:只有一个线程反复进同一个锁,把线程 ID 写在 Mark Word 里,之后进来连 CAS 都省了。JDK 15 起默认关闭、JDK 18 起彻底废弃(撤销成本高于收益),新项目直接按「无锁 → 轻量 → 重量」理解即可,但面试仍会问,答的时候补一句「JDK 15+ 已默认禁用偏向锁」会加分。
  • 轻量级锁:在当前线程栈帧里建 Lock Record,CAS 把 Mark Word 指向它;失败就自旋,自旋到阈值(自适应自旋)还不行就膨胀。
  • 重量级锁:竞争线程进 _EntryList 排队,由操作系统 mutex 挂起 —— 涉及用户态/内核态切换,所以「synchronized 慢」指的是这一层。

4.4 锁优化 ​

  • 锁消除:逃逸分析发现锁对象不会逃出方法,直接去掉(如方法内 new Object() 加锁)。
  • 锁粗化:循环里反复加解锁会被合并成一次。
  • 自适应自旋:根据上次自旋成功率决定这次自旋多久。

4.5 synchronized vs ReentrantLock ​

synchronizedReentrantLock
实现JVM 关键字,monitorJDK 类,基于 AQS
释放自动(异常也释放)必须 finally { unlock(); }
可中断不支持lockInterruptibly()
超时不支持tryLock(timeout)
公平锁只有非公平可选公平 / 非公平
条件队列一个 wait/notify多个 Condition,可精准唤醒
性能JDK6 后优化得差不多高竞争下更可控

结论口径:能不用显式锁就不用;需要超时、可中断、多条件队列时才上 ReentrantLock。

5. CAS 与原子类 ​

5.1 CAS 是什么 ​

java
// 伪代码:只有当 value 等于 expected 时才更新为 newValue
boolean cas(int expected, int newValue)
// 伪代码:只有当 value 等于 expected 时才更新为 newValue
boolean cas(int expected, int newValue)

对应 CPU 的 cmpxchg 指令,Java 里通过 Unsafe.compareAndSwapInt(JDK9+ 是 VarHandle)调用。它是乐观锁:先假设没人改,改的时候校验,失败就重试(自旋)。

5.2 三个问题 ​

  1. ABA:值从 A→B→A,CAS 察觉不到。解决:AtomicStampedReference 带版本号。
  2. 自旋开销:高竞争下大量空转。解决:LongAdder 分段、或者直接上锁。
  3. 只能保证一个变量:多个变量要么合并成一个对象用 AtomicReference,要么加锁。

5.3 AtomicInteger 与 LongAdder ​

java
AtomicInteger ai = new AtomicInteger(0);
ai.incrementAndGet();          // 底层就是 CAS 自旋

LongAdder adder = new LongAdder();   // 高并发计数首选
adder.increment();
long sum = adder.sum();              // 汇总所有 Cell
AtomicInteger ai = new AtomicInteger(0);
ai.incrementAndGet();          // 底层就是 CAS 自旋

LongAdder adder = new LongAdder();   // 高并发计数首选
adder.increment();
long sum = adder.sum();              // 汇总所有 Cell

LongAdder 把热点值拆成 base + Cell[],不同线程 CAS 不同的 Cell,读的时候再累加。写多读少用它,读多写少用 AtomicLong。

6. AQS:整个 JUC 锁的地基 ​

6.1 核心结构 ​

java
public abstract class AbstractQueuedSynchronizer {
    private volatile int state;          // 同步状态,语义由子类定义
    private transient volatile Node head; // 同步队列头
    private transient volatile Node tail; // 同步队列尾
}
public abstract class AbstractQueuedSynchronizer {
    private volatile int state;          // 同步状态,语义由子类定义
    private transient volatile Node head; // 同步队列头
    private transient volatile Node tail; // 同步队列尾
}
  • state:ReentrantLock 里是重入次数,Semaphore 里是可用许可数,CountDownLatch 里是剩余计数。
  • 同步队列:CLH 变体,双向链表,抢不到锁的线程包装成 Node 排队,LockSupport.park() 挂起。
  • 条件队列:Condition.await() 把线程从同步队列移到条件队列,signal() 再移回来重新排队抢锁。

6.2 独占与共享 ​

子类只需实现 tryAcquire / tryRelease(独占)或 tryAcquireShared / tryReleaseShared(共享),AQS 负责排队、阻塞、唤醒。这就是模板方法模式。

java
// 以 ReentrantLock 非公平锁为例(简化)
final boolean nonfairTryAcquire(int acquires) {
    Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {   // 直接抢,不排队 —— 这就是"非公平"
            setExclusiveOwnerThread(current);
            return true;
        }
    } else if (current == getExclusiveOwnerThread()) {
        setState(c + acquires);                  // 重入,直接加 state
        return true;
    }
    return false;
}
// 以 ReentrantLock 非公平锁为例(简化)
final boolean nonfairTryAcquire(int acquires) {
    Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {   // 直接抢,不排队 —— 这就是"非公平"
            setExclusiveOwnerThread(current);
            return true;
        }
    } else if (current == getExclusiveOwnerThread()) {
        setState(c + acquires);                  // 重入,直接加 state
        return true;
    }
    return false;
}

公平 vs 非公平:公平锁在 tryAcquire 里先 hasQueuedPredecessors() 判断有没有人在排队,有就不抢;非公平锁上来就 CAS。非公平吞吐更高,代价是可能饿死排队线程。

6.3 基于 AQS 的四个常用工具 ​

工具state 语义模式典型用法
ReentrantLock重入次数独占替代 synchronized,需要超时/中断
ReentrantReadWriteLock高 16 位读、低 16 位写共享 + 独占读多写少;注意写锁可降级、读锁不能升级
Semaphore剩余许可数共享限流、连接池
CountDownLatch剩余计数(不可重置)共享一个线程等 N 个任务完成
java
// CountDownLatch:主线程等 3 个初始化完成
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
    new Thread(() -> { init(); latch.countDown(); }).start();
}
latch.await();   // 归零前一直阻塞
// CountDownLatch:主线程等 3 个初始化完成
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
    new Thread(() -> { init(); latch.countDown(); }).start();
}
latch.await();   // 归零前一直阻塞

CyclicBarrier 不是 AQS 实现(用 ReentrantLock + Condition),区别是可重用,且是「线程互相等」。

7. ThreadLocal:线程私有的上下文 ​

7.1 结构 ​

Thread --持有--> ThreadLocalMap(每个线程独一份)
                    └── Entry[] table,key 是 ThreadLocal 的弱引用,value 是强引用
Thread --持有--> ThreadLocalMap(每个线程独一份)
                    └── Entry[] table,key 是 ThreadLocal 的弱引用,value 是强引用
java
ThreadLocal<User> holder = new ThreadLocal<>();
holder.set(user);            // 存到当前线程的 map 里
User u = holder.get();
holder.remove();             // 一定要 remove
ThreadLocal<User> holder = new ThreadLocal<>();
holder.set(user);            // 存到当前线程的 map 里
User u = holder.get();
holder.remove();             // 一定要 remove

ThreadLocalMap 用开放寻址(线性探测)解决冲突,不用链表 —— 因为 entry 少。

7.2 内存泄漏链路 ​

Thread → ThreadLocalMap → Entry(key 弱引用, value 强引用) → value 对象
Thread → ThreadLocalMap → Entry(key 弱引用, value 强引用) → value 对象

ThreadLocal 对象被回收后,key 变 null,但 value 还被 Entry 强引用,只要线程不死(尤其是线程池里的核心线程),value 就一直泄漏。所以:

用完必须 remove()。线程池里不 remove,下一个任务可能读到上一个用户的身份信息 —— 这既是内存泄漏,也是越权 bug。

7.3 InheritableThreadLocal 与 TTL ​

  • InheritableThreadLocal:在 new Thread() 时把父线程的 map 浅拷贝一份给子线程 —— 只在线程创建时生效,线程池复用线程时失效。
  • TransmittableThreadLocal(阿里 TTL):配合 TtlExecutors 包装线程池,在任务提交和执行的瞬间做上下文快照/回放,解决线程池传递问题。

8. 线程池:最容易被追问的一节 ​

8.1 七个参数 ​

java
new ThreadPoolExecutor(
    corePoolSize,        // 核心线程数,常驻(allowCoreThreadTimeOut 可回收)
    maximumPoolSize,     // 最大线程数
    keepAliveTime,       // 非核心线程空闲存活时间
    TimeUnit.SECONDS,
    workQueue,           // 等待队列
    threadFactory,       // 线程工厂,务必自定义名字,排查靠它
    handler              // 拒绝策略
);
new ThreadPoolExecutor(
    corePoolSize,        // 核心线程数,常驻(allowCoreThreadTimeOut 可回收)
    maximumPoolSize,     // 最大线程数
    keepAliveTime,       // 非核心线程空闲存活时间
    TimeUnit.SECONDS,
    workQueue,           // 等待队列
    threadFactory,       // 线程工厂,务必自定义名字,排查靠它
    handler              // 拒绝策略
);

8.2 执行流程(背下来) ​

提交任务
  ├─ 当前线程数 < core            → 新建核心线程执行
  ├─ 核心已满 → 尝试入队
  │     ├─ 入队成功                → 等核心线程来取
  │     └─ 队列满 → 线程数 < max   → 新建非核心线程执行
  └─ 队列满且线程数 = max          → 触发拒绝策略
提交任务
  ├─ 当前线程数 < core            → 新建核心线程执行
  ├─ 核心已满 → 尝试入队
  │     ├─ 入队成功                → 等核心线程来取
  │     └─ 队列满 → 线程数 < max   → 新建非核心线程执行
  └─ 队列满且线程数 = max          → 触发拒绝策略

为什么先入队而不是先开满线程:创建线程成本高(栈内存 + 内核调度),队列能缓冲突发流量;只有在队列也满了才说明真的扛不住。

8.3 四种拒绝策略 ​

策略行为适合
AbortPolicy(默认)抛 RejectedExecutionException必须让上游感知失败
CallerRunsPolicy提交任务的线程自己跑削峰,天然限速
DiscardPolicy静默丢弃日志、埋点等可丢数据
DiscardOldestPolicy丢队头最老的,再重试提交只要最新数据

8.4 为什么禁用 Executors 的默认工厂 ​

  • newFixedThreadPool / newSingleThreadExecutor:队列是 LinkedBlockingQueue,无界,任务堆积到 OOM。
  • newCachedThreadPool:最大线程数 Integer.MAX_VALUE,无限创建线程,直接 OOM。

面试口径:必须手写 ThreadPoolExecutor,队列要有界,线程数要按业务算,并且给线程起名。

8.5 线程数怎么定 ​

  • CPU 密集(计算、序列化):N + 1,N 是核数。
  • IO 密集(RPC、DB、文件):N × (1 + 等待时间 / 计算时间),实测 2N ~ 4N 起步再压测。
  • 混合型按瓶颈拆分两个池,别用一个池扛所有。

8.6 几个坑 ​

java
// 坑 1:submit 吞异常 —— 异常被封在 Future 里
executor.submit(() -> { throw new RuntimeException("boom"); });   // 主线程毫无感知
Future<?> f = executor.submit(task);
f.get();                                  // 这样才抛出来
// 或者 execute():异常会走到 UncaughtExceptionHandler

// 坑 2:线程池里 ThreadLocal 脏读 —— 任务结束必须 remove
// 坑 3:关闭要用 shutdown() 而不是 shutdownNow()(后者会中断正在跑的任务)
// 坑 1:submit 吞异常 —— 异常被封在 Future 里
executor.submit(() -> { throw new RuntimeException("boom"); });   // 主线程毫无感知
Future<?> f = executor.submit(task);
f.get();                                  // 这样才抛出来
// 或者 execute():异常会走到 UncaughtExceptionHandler

// 坑 2:线程池里 ThreadLocal 脏读 —— 任务结束必须 remove
// 坑 3:关闭要用 shutdown() 而不是 shutdownNow()(后者会中断正在跑的任务)

8.7 监控 ​

java
ThreadPoolExecutor pool = ...;
pool.getActiveCount();       // 活跃线程
pool.getQueue().size();      // 积压任务数  ← 最该告警的指标
pool.getCompletedTaskCount();
pool.getPoolSize();
ThreadPoolExecutor pool = ...;
pool.getActiveCount();       // 活跃线程
pool.getQueue().size();      // 积压任务数  ← 最该告警的指标
pool.getCompletedTaskCount();
pool.getPoolSize();

把 queue.size / queue.remainingCapacity 打到 Prometheus,超过 70% 就告警 —— 这是线上最有效的一道防线。

9. 并发容器 ​

9.1 ConcurrentHashMap(JDK 1.8) ​

版本并发控制
1.7Segment 分段锁,默认 16 段
1.8CAS 初始化 + synchronized 锁桶头节点 + 红黑树

关键点:

  • 锁粒度到「链表/树的头节点」,不是整表。
  • size() 用 baseCount + CounterCell[] 分散计数(和 LongAdder 同思路)。
  • get() 不加锁,靠 volatile 读 Node 的 val 和 next。
  • 不允许 null key / null value —— 因为并发下无法区分「不存在」和「值是 null」。

9.2 CopyOnWriteArrayList ​

写时复制整个数组,读完全无锁。只适合读多写极少(白名单、监听器列表),写多了会疯狂 GC。

9.3 BlockingQueue 家族 ​

实现特点场景
ArrayBlockingQueue有界数组,单锁两 Condition线程池队列(推荐)
LinkedBlockingQueue链表,默认无界(坑)一定传容量
SynchronousQueue不存储,直接交接newCachedThreadPool
PriorityBlockingQueue无界优先队列按优先级消费
DelayQueue延迟到期才能取本地延迟任务
LinkedTransferQueue无锁,transfer 语义高吞吐生产者消费者

10. CompletableFuture:异步编排 ​

java
CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(() -> userService.get(id), pool);
CompletableFuture<List<Order>> orderFuture = CompletableFuture.supplyAsync(() -> orderService.list(id), pool);

// 两个并行、都完成后再合并
CompletableFuture<UserDetail> detail = userFuture
    .thenCombine(orderFuture, (u, orders) -> new UserDetail(u, orders))
    .exceptionally(e -> UserDetail.empty())           // 兜底
    .completeOnTimeout(UserDetail.empty(), 500, TimeUnit.MILLISECONDS);  // 超时兜底
CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(() -> userService.get(id), pool);
CompletableFuture<List<Order>> orderFuture = CompletableFuture.supplyAsync(() -> orderService.list(id), pool);

// 两个并行、都完成后再合并
CompletableFuture<UserDetail> detail = userFuture
    .thenCombine(orderFuture, (u, orders) -> new UserDetail(u, orders))
    .exceptionally(e -> UserDetail.empty())           // 兜底
    .completeOnTimeout(UserDetail.empty(), 500, TimeUnit.MILLISECONDS);  // 超时兜底

记住三组:

需求方法
串行thenApply(有返回值)/ thenAccept(无)/ thenRun(不关心结果)
并行合并thenCombine(两个)/ allOf(多个,无返回值)/ anyOf
异常与超时exceptionally / handle / whenComplete / completeOnTimeout

必须传自定义线程池,否则用 ForkJoinPool.commonPool(),核心数 - 1 个线程,被 IO 任务占满会拖垮全站。

10.1 allOf 最大的坑:它不会抛异常 ​

java
CompletableFuture<Void> all = CompletableFuture.allOf(f1, f2, f3);
all.get();   // ❌ 某个子任务异常时,这里可能感知不到

// ✅ 正确姿势:allOf 只负责「都结束」,异常要自己收
CompletableFuture.allOf(f1, f2, f3)
    .exceptionally(e -> { log.error("有任务失败", e); return null; })
    .join();
// 或者逐个 join,让异常冒出来
f1.join(); f2.join(); f3.join();
CompletableFuture<Void> all = CompletableFuture.allOf(f1, f2, f3);
all.get();   // ❌ 某个子任务异常时,这里可能感知不到

// ✅ 正确姿势:allOf 只负责「都结束」,异常要自己收
CompletableFuture.allOf(f1, f2, f3)
    .exceptionally(e -> { log.error("有任务失败", e); return null; })
    .join();
// 或者逐个 join,让异常冒出来
f1.join(); f2.join(); f3.join();

原因:allOf 返回的是「组合后的 future」,子任务的异常被包在各自的 future 里,不 join 就不会抛。

10.2 带 Async 后缀和不带的区别 ​

写法回调在哪个线程跑
thenApply(fn)当前完成的那个线程(可能是任意线程)
thenApplyAsync(fn)提交到线程池(默认 commonPool)
thenApplyAsync(fn, pool)提交到指定线程池

坑:thenApply 如果跟在 supplyAsync(ctx) 后面,回调可能跑在业务线程池的线程里 —— 如果在回调里做阻塞 IO,就会占满那个池。需要隔离时一律显式传池。

10.3 不要在 CompletableFuture 里忘记兜底 ​

java
// ❌ 没有超时、没有兜底:下游卡住 → 整条链路一起卡
CompletableFuture.supplyAsync(() -> remote.call(), pool).join();

// ✅ 超时 + 兜底 + 异常日志
CompletableFuture.supplyAsync(() -> remote.call(), pool)
    .completeOnTimeout(Default.INSTANCE, 300, TimeUnit.MILLISECONDS)
    .exceptionally(e -> { log.warn("降级", e); return Default.INSTANCE; })
    .join();
// ❌ 没有超时、没有兜底:下游卡住 → 整条链路一起卡
CompletableFuture.supplyAsync(() -> remote.call(), pool).join();

// ✅ 超时 + 兜底 + 异常日志
CompletableFuture.supplyAsync(() -> remote.call(), pool)
    .completeOnTimeout(Default.INSTANCE, 300, TimeUnit.MILLISECONDS)
    .exceptionally(e -> { log.warn("降级", e); return Default.INSTANCE; })
    .join();

11. 三个高频并发模式 ​

11.1 生产者消费者 ​

java
// 生产环境直接用 BlockingQueue,别手写 wait/notify
BlockingQueue<Task> queue = new ArrayBlockingQueue<>(1000);

// 生产
queue.put(task);          // 满了阻塞;offer 是非阻塞版
// 消费
Task t = queue.take();    // 空了阻塞;poll 是非阻塞版
// 生产环境直接用 BlockingQueue,别手写 wait/notify
BlockingQueue<Task> queue = new ArrayBlockingQueue<>(1000);

// 生产
queue.put(task);          // 满了阻塞;offer 是非阻塞版
// 消费
Task t = queue.take();    // 空了阻塞;poll 是非阻塞版

手写版(面试让写就用这个):while 判断条件 + wait() + notifyAll(),三个细节缺一不可。

11.2 单例的并发正确姿势 ​

java
// 首选:静态内部类(懒加载 + 线程安全,靠类加载机制保证)
public class Singleton {
    private Singleton() {}
    private static class Holder { static final Singleton INSTANCE = new Singleton(); }
    public static Singleton getInstance() { return Holder.INSTANCE; }
}

// 次选:枚举(防反射、防反序列化破坏)
public enum SingletonEnum {
    INSTANCE;
    public void doSomething() {}
}
// 首选:静态内部类(懒加载 + 线程安全,靠类加载机制保证)
public class Singleton {
    private Singleton() {}
    private static class Holder { static final Singleton INSTANCE = new Singleton(); }
    public static Singleton getInstance() { return Holder.INSTANCE; }
}

// 次选:枚举(防反射、防反序列化破坏)
public enum SingletonEnum {
    INSTANCE;
    public void doSomething() {}
}

11.3 并发限流(Semaphore 版) ​

java
Semaphore permits = new Semaphore(10);   // 最多 10 个并发

if (permits.tryAcquire(200, TimeUnit.MILLISECONDS)) {
    try { doWork(); } finally { permits.release(); }
} else {
    // 排队超时,走降级
}
Semaphore permits = new Semaphore(10);   // 最多 10 个并发

if (permits.tryAcquire(200, TimeUnit.MILLISECONDS)) {
    try { doWork(); } finally { permits.release(); }
} else {
    // 排队超时,走降级
}

12. 线上并发问题排查 ​

12.1 死锁 ​

bash
jstack <pid> | grep -A 20 "Found one Java-level deadlock"
jstack <pid> | grep -A 20 "Found one Java-level deadlock"

输出里会直接给出互相等待的两个线程和持有的锁。代码层面:所有线程按同一顺序加锁、用 tryLock(timeout) 代替 lock()。

12.2 CPU 100% ​

bash
top -Hp <pid>                 # 找到最耗 CPU 的线程 tid
printf '%x\n' <tid>           # 转 16 进制
jstack <pid> | grep -A 30 <hex-tid>
top -Hp <pid>                 # 找到最耗 CPU 的线程 tid
printf '%x\n' <tid>           # 转 16 进制
jstack <pid> | grep -A 30 <hex-tid>

看栈顶是业务死循环、GC 线程(说明内存有问题)、还是正则回溯。

12.3 线程池打满 ​

现象:接口 RT 飙升但 CPU 不高、getActiveCount() 顶到 max、队列持续增长。排查顺序:

  1. 队列大小和拒绝计数(有没有在丢任务)
  2. 任务里有没有阻塞调用(DB 慢查询、同步 HTTP、锁等待)
  3. 线程栈里都在等什么:jstack | grep -c "waiting on condition"

13. 虚拟线程与并行流 ​

13.1 虚拟线程(JDK 21) ​

是什么:由 JVM 调度的轻量线程,大量虚拟线程跑在少量「载体线程」上(M:N)。创建成本约几百字节,可以几十万个。

解决什么问题:平台线程(1:1 映射内核线程)很贵 —— 几千个就吃满内存,而且一旦阻塞就白占一个内核线程。虚拟线程遇到阻塞 IO 时会自动从载体线程上卸载,载体线程立刻去跑别的虚拟线程。

java
// 推荐:每个任务一个虚拟线程(不要池化)
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    IntStream.range(0, 10_000).forEach(i ->
        executor.submit(() -> httpClient.send(request, BodyHandlers.ofString())));
}
// 推荐:每个任务一个虚拟线程(不要池化)
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    IntStream.range(0, 10_000).forEach(i ->
        executor.submit(() -> httpClient.send(request, BodyHandlers.ofString())));
}
平台线程虚拟线程
数量级几千几十万
调度者操作系统JVM
阻塞代价占住内核线程卸载,载体线程继续干活
适合CPU 密集IO 密集、每请求一线程

三个坑:

  1. 不要池化虚拟线程(池化就失去了「便宜」的意义,直接 per-task 创建);
  2. synchronized 里长时间阻塞会 pin 住载体线程(JDK 21 的限制,JDK 24 已大幅改善)—— 高并发场景改用 ReentrantLock;
  3. ThreadLocal 慎用:几十万虚拟线程各存一份会爆内存,改用 ScopedValue 或显式传参。

13.2 ForkJoinPool 与 parallelStream ​

工作窃取(work-stealing):每个 worker 有自己的双端队列,任务被 fork 成子任务压入自己的队列;空闲的 worker 从别人队列的尾部偷任务。适合「能递归拆分的纯计算」。

java
// parallelStream 用的就是 ForkJoinPool.commonPool()
list.parallelStream().map(this::heavyCompute).collect(toList());
// parallelStream 用的就是 ForkJoinPool.commonPool()
list.parallelStream().map(this::heavyCompute).collect(toList());

并行流的四个坑:

坑说明
在流里做阻塞 IO会占满 commonPool(并行度 = 核数 - 1),拖垮全站其他并行流
共用 commonPool所有 parallelStream 抢同一个池,互相影响
有状态/顺序敏感forEach 不保证顺序,共享可变状态会竞态(要用 collect)
数据量小反而更慢拆分与合并有开销,几个元素的列表用 for 循环

结论:CPU 密集 + 数据量大 → 可以用;涉及 IO → 用自定义线程池或虚拟线程。

13.3 StampedLock(读多写少的利器) ​

java
StampedLock lock = new StampedLock();

// 乐观读:完全不加锁,读完校验版本
long stamp = lock.tryOptimisticRead();
int x = state.x;
if (!lock.validate(stamp)) {          // 期间有人写过 → 退化成悲观读
    stamp = lock.readLock();
    try { x = state.x; } finally { lock.unlockRead(stamp); }
}
StampedLock lock = new StampedLock();

// 乐观读:完全不加锁,读完校验版本
long stamp = lock.tryOptimisticRead();
int x = state.x;
if (!lock.validate(stamp)) {          // 期间有人写过 → 退化成悲观读
    stamp = lock.readLock();
    try { x = state.x; } finally { lock.unlockRead(stamp); }
}
ReentrantReadWriteLockStampedLock
读模式悲观读(要加锁)悲观读 + 乐观读(不加锁)
可重入✅❌ 不可重入
支持条件变量✅❌
适用通用读远多于写、且能接受不可重入

14. 速查表 ​

问题一句话答案
volatile 能保证原子性吗不能,只能保证可见性和有序性
synchronized 锁的是什么对象头 Mark Word + monitor;锁代码块锁的是括号里的对象
锁能降级吗不能,只能升级
AQS 的 state 是什么由子类定义的同步状态:重入次数 / 许可数 / 剩余计数
线程池先入队还是先扩容先入队,队列满了才扩到 max
为什么不用 Executors队列无界或线程无上限,都会 OOM
ThreadLocal 为什么会泄漏key 弱引用 value 强引用,线程不死 value 不释放
CountDownLatch 和 CyclicBarrier 区别前者一次性、一个等多个;后者可重用、互相等
CompletableFuture 用哪个线程池必须传自定义的,默认 commonPool 只有 N-1 个线程
CAS 的 ABA 怎么解决加版本号,AtomicStampedReference