上一篇写了并发基础,synchronized 和 volatile 属于入门。

但面试官显然不满足。

有一次被追问:“ReentrantLock 和 synchronized 的区别?底层是什么实现的?”

我说了一堆 API 层的区别,面试官一脸嫌弃说:“AQS 知道吗?”

我卡了三秒。那次面试凉了。

回来我专门啃了 JUC 的源码,这篇把 JUC 里最值得搞懂的东西整理一遍。


JUC 是什么

java.util.concurrent,简称 JUC。

JDK 1.5 才加进来,Doug Lea 写的。专门解决高并发场景下的各种问题。

核心内容分三块:

JUC
├── 锁:ReentrantLock、ReadWriteLock、StampedLock
├── 工具类:CountDownLatch、CyclicBarrier、Semaphore
└── 并发集合:ConcurrentHashMap、BlockingQueue、CopyOnWriteArrayList

这三块都建立在一个核心组件上:AQS。

搞懂 AQS,JUC 的大半东西就通了。


AQS:JUC 的底层基石

AQS,全称 AbstractQueuedSynchronizer,抽象队列同步器。

别被名字吓到。它本质就干一件事:管理一个同步状态(state),然后提供一套排队机制。

AQS 的核心结构

AQS
├── state:volatile int,同步状态
│      0 = 无锁,1 = 有锁,>1 = 重入次数
│
└── CLH 队列:等待获取锁的线程,排成双向链表
       head → Node(Thread-1) ↔ Node(Thread-2) ↔ ... ← tail

两个核心操作:

  • acquire():尝试拿锁,拿不到就进队列等
  • release():释放锁,唤醒队列头部的线程

子类通过重写 tryAcquire() 和 tryRelease() 来实现不同的锁语义。

独占模式 vs 共享模式

独占模式:同一时刻只有一个线程能拿到锁。ReentrantLock 就是这种。

共享模式:同一时刻允许多个线程持有锁。CountDownLatch、Semaphore 是这种。

面试问"AQS 是干什么的",答:AQS 是一个通用的同步框架,通过 volatile state 和 CLH 队列实现了独占和共享两种锁语义,ReentrantLock 等工具类在它上面封装。


ReentrantLock:比 synchronized 更灵活

基本用法

ReentrantLock lock = new ReentrantLock();

lock.lock();
try {
    // 临界区
} finally {
    lock.unlock(); // 必须在finally里,否则异常时锁不释放
}

我踩过的坑:

写了个并发统计代码,lock 之后忘了 finally 里 unlock。

某次请求抛了异常,锁没释放,后续所有请求全部挂起,整个服务卡死。

排查了半小时,看了线程 dump 才发现所有线程都 BLOCKED 在同一把锁上。

从那以后,lock 和 unlock 必须写成 try-finally 一对。

ReentrantLock vs synchronized

对比项synchronizedReentrantLock
来源JVM 关键字JDK 类库
中断等待不能可以(lockInterruptibly)
超时获锁不能可以(tryLock(time))
公平锁不支持支持(new ReentrantLock(true))
条件变量Object.wait/notifyCondition
性能JDK 6 后差不多差不多
手动释放不需要(自动)必须手动 unlock

公平锁是什么?

默认 ReentrantLock 是非公平的,谁抢到就是谁的,不管排队顺序。

公平锁按 CLH 队列顺序,先来先得。公平锁延迟高一点,吞吐量低。

一般不用公平锁,除非对线程优先级有严格要求。

Condition:精准唤醒

synchronized 配套的是 wait/notify,ReentrantLock 配套的是 Condition。

Condition 比 notify 强的地方:可以针对不同条件,精准唤醒指定的等待线程。

ReentrantLock lock = new ReentrantLock();
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();

// 生产者
lock.lock();
try {
    while (queue.isFull()) {
        notFull.await();     // 只等"非满"这个条件
    }
    queue.add(item);
    notEmpty.signal();       // 只唤醒等"非空"的消费者
} finally {
    lock.unlock();
}

如果用 notify(),随机唤醒,可能唤醒到同类(生产者唤醒生产者),白唤醒一次。

Condition 可以直接点名叫相关的等待者,效率更高。


CountDownLatch:等待一组任务完成

CountDownLatch(N) 相当于一个倒计数门栓,数到 0 才放行。

CountDownLatch latch = new CountDownLatch(3);

// 三个子任务线程
for (int i = 0; i < 3; i++) {
    new Thread(() -> {
        doWork();
        latch.countDown(); // 每个子任务完成后 count -1
    }).start();
}

latch.await(); // 主线程在这等,直到 count = 0
System.out.println("三个子任务都完成了");

适用场景: 主线程等多个子线程全部完成后再汇总。

底层: AQS 共享模式。初始 state = N,每次 countDown() 对 state CAS -1,减到 0 后唤醒所有在 await() 等待的线程。

一个注意点: CountDownLatch 不能复用,count 到 0 就结束了,不能重置。需要复用的场景用 CyclicBarrier。


CyclicBarrier:等所有线程到齐再出发

CountDownLatch 是一次性的,CyclicBarrier 可以反复用。

CyclicBarrier barrier = new CyclicBarrier(3, () -> {
    // 所有线程到达屏障点后,这段代码先跑一次
    System.out.println("所有人就位,游戏开始!");
});

for (int i = 0; i < 3; i++) {
    new Thread(() -> {
        System.out.println(Thread.currentThread().getName() + " 准备好了");
        try {
            barrier.await(); // 等待其他人到齐
        } catch (Exception e) {}
        System.out.println(Thread.currentThread().getName() + " 开始游戏");
    }).start();
}

CountDownLatch vs CyclicBarrier:

  • CountDownLatch:一个线程等多个线程,只能用一次
  • CyclicBarrier:多个线程互相等,可以复用

Semaphore:控制并发数量

Semaphore 是信号量,控制同时访问某个资源的线程数量。

Semaphore sem = new Semaphore(3); // 最多允许 3 个线程同时跑

for (int i = 0; i < 10; i++) {
    new Thread(() -> {
        try {
            sem.acquire(); // 拿到许可证
            doWork();      // 访问资源
        } catch (InterruptedException e) {
        } finally {
            sem.release(); // 归还许可证
        }
    }).start();
}

10 个线程,但最多 3 个同时跑。

适用场景: 数据库连接池、限流、接口限速。

底层: AQS 共享模式。state 是许可证数量,acquire() 对 state CAS -1,release() 对 state +1,state < 0 就进队列等。


ReadWriteLock:读多写少的优化

synchronized 和 ReentrantLock 都是排他锁:不管读写,同一时刻只允许一个线程。

读多写少的场景(比如缓存),这样太浪费。

ReadWriteLock 允许:

  • 多个读线程同时持有读锁
  • 写锁是排他的,写的时候读线程也等
ReadWriteLock rwLock = new ReentrantReadWriteLock();
Lock readLock = rwLock.readLock();
Lock writeLock = rwLock.writeLock();

// 读操作
readLock.lock();
try {
    return cache.get(key);
} finally {
    readLock.unlock();
}

// 写操作
writeLock.lock();
try {
    cache.put(key, value);
} finally {
    writeLock.unlock();
}

读写锁的性能比读写都用 synchronized 高很多,读线程多的时候尤其明显。

我踩过的坑:

有一次实现缓存,读写都用了读锁:

rwLock.readLock().lock();
try {
    if (!cache.containsKey(key)) {
        // 这里需要写,但是只持有读锁!
        cache.put(key, loadFromDB(key)); // 死锁或数据不一致
    }
    return cache.get(key);
} finally {
    rwLock.readLock().unlock();
}

在读锁里想升级成写锁,是不被允许的,会死锁。

正确方法:先释放读锁,再获取写锁,或者一开始就用写锁。


并发集合:别用 Collections.synchronized

工程里处理并发安全的集合,不要用 Collections.synchronizedMap(new HashMap<>())。

这种写法是给 HashMap 套了个全局锁,读写全是串行,吞吐量极差。

JUC 提供的并发集合:

集合特点适用场景
ConcurrentHashMap分段 CAS 锁,高性能高并发读写 Map
CopyOnWriteArrayList写时复制,读不加锁读多写极少的 List
LinkedBlockingQueue有界阻塞队列生产者-消费者
ArrayBlockingQueue固定容量阻塞队列线程池任务队列
ConcurrentLinkedQueue无界无锁队列,CAS 实现高并发、不需要阻塞

CopyOnWriteArrayList 怎么实现的?

写操作时,先复制一份新数组,在新数组上修改,修改完用新数组替换旧数组。

读操作完全不加锁,读的是旧数组的引用,所以并发读没有任何阻塞。

代价是:写操作内存开销大,而且写完之前的读操作看不到新数据(弱一致性)。


面试高频问题

Q1:AQS 的核心原理是什么?

AQS 维护了一个 volatile int state 表示同步状态,以及一个 CLH 双向队列存放等待线程。通过 CAS 修改 state 来实现锁的获取和释放,拿不到锁的线程进队列等待,释放锁时唤醒队头线程。

Q2:ReentrantLock 怎么实现可重入的?

每次 lock() 时,检查当前持有锁的线程是不是自己。是的话 state + 1,unlock() 时 state - 1,减到 0 才真正释放锁。

Q3:CountDownLatch 和 CyclicBarrier 的区别?

CountDownLatch 是一个线程等多个子线程完成,只能用一次,不可重置。CyclicBarrier 是多个线程互相等待到齐,可以重置复用。

Q4:为什么 ConcurrentHashMap 不允许 key 为 null?

HashMap 允许 null 是因为单线程环境下 get(null) 返回 null 可以通过 containsKey(null) 判断到底是 key 不存在还是 value 是 null。但并发场景下,containsKey 和 get 之间可能有线程修改了数据,这个二义性无法解决,所以直接禁止。

Q5:Semaphore 有什么实际用途?

数据库连接池(限制并发连接数)、接口限流(限制每秒并发请求数)、停车场模型(固定车位数)。


记忆口诀

AQS 是地基,state 加队列

ReentrantLock 可中断,Condition 点名叫

CountDownLatch 一次性,Barrier 可循环

Semaphore 控并发,读写锁提性能


总结

JUC 这块,面试问的基本就这几个方向:

第一:AQS 原理。 面试官的底线,说不出来直接扣分。

第二:各工具类怎么选。 什么场景用 CountDownLatch,什么场景用 CyclicBarrier,不能混。

第三:并发集合。 ConcurrentHashMap、CopyOnWriteArrayList 的实现原理和适用场景。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐