本文完整合并「Java锁专题」与「并发工具类专题」核心内容,重点补充二者的底层共性、实现依赖、功能互补、场景协同等核心关联,从底层原理到实战落地,形成一套完整的Java并发编程知识体系,兼顾日常开发与面试高频考点。
开篇:并发编程的核心矛盾与两大核心支柱
1. 并发编程的核心矛盾
Java并发编程的本质,是解决多线程环境下的两大核心问题:
竞态问题:多个线程同时读写共享资源时,会出现原子性、可见性、有序性被破坏的问题,导致数据脏读、丢失更新等线程安全事故;
协作问题:多个线程需要按照业务规则协同执行(如等待、唤醒、顺序执行、并行汇总、异步编排等),否则会出现执行逻辑混乱、结果不符合预期的问题。
2. 两大核心支柱:锁体系 + 并发工具类
这两大组件是Java并发编程的完整骨架,二者不是孤立存在,而是相辅相成、深度绑定的共生关系:
锁体系:是并发编程的「基础基石」,核心解决竞态问题,通过互斥机制保证共享资源的线程安全,是所有多线程协作的前提;
并发工具类:是并发编程的「高阶封装」,核心解决协作问题,基于锁体系的底层能力,封装了开箱即用的多线程同步、编排、通信能力,避免开发者手动基于锁实现复杂协作逻辑。
3. 底层共同基石:AQS抽象队列同步器
在展开详细内容前,必须先明确二者最核心的底层关联:JUC显式锁体系(ReentrantLock、读写锁)和绝大多数并发工具类(CountDownLatch、Semaphore、CyclicBarrier),底层都基于AbstractQueuedSynchronizer(AQS,抽象队列同步器)实现。
AQS是JUC并发包的核心,它封装了一套通用的同步机制:
核心状态变量:
volatile int state,标记同步状态,所有子类通过修改这个状态实现加锁/解锁、同步计数等逻辑;核心数据结构:CLH双向FIFO等待队列,存放获取同步状态失败的阻塞线程,负责线程的排队、唤醒、上下文切换;
两种核心模式:独占模式(同一时间只能有一个线程持有同步状态,用于锁实现)、共享模式(同一时间可以有多个线程持有同步状态,用于并发工具类实现)。
第一部分:Java锁体系全解析(解决竞态问题)
锁的核心使命,是通过互斥机制,保证多线程环境下共享资源操作的原子性、可见性、有序性,从根源上避免线程安全问题。
1. 锁的全景分类
先建立全局认知,Java锁可按核心特性划分为以下类别,所有分类都围绕「解决竞态问题的不同策略」展开:
2. synchronized:JVM内置隐式锁
2.1 是什么
synchronized 是Java原生内置、基于JVM实现的隐式独占可重入悲观锁,无需手动管理加锁与释放,JVM自动完成,是Java并发编程最基础、最常用的锁实现。
2.2 为什么用
使用极简:无需手动释放锁,不会因代码异常导致锁泄漏,开发成本极低;
JVM深度优化:JDK1.6引入锁升级、锁消除、锁粗化等机制,无竞争场景性能拉满,绝大多数场景下与显式锁持平;
原生支持:JVM天然支持,无需引入第三方依赖,兼容性极强。
2.3 怎么用
有3种标准用法,锁粒度从粗到细可控,核心原则是锁对象必须是不可变的:
public class SynchronizedDemo {
private static int staticCount = 0;
private int instanceCount = 0;
private final Object lockObj = new Object(); // 自定义不可变锁对象
// 用法1:修饰实例方法,锁对象为当前this实例,仅作用于当前实例
public synchronized void instanceMethod() {
instanceCount++;
System.out.println("实例方法加锁,count=" + instanceCount);
}
// 用法2:修饰静态方法,锁对象为当前类的Class对象,作用于所有实例
public static synchronized void staticMethod() {
staticCount++;
System.out.println("静态方法加锁,count=" + staticCount);
}
// 用法3:修饰代码块,手动指定锁对象,粒度最细,推荐优先使用
public void codeBlockMethod() {
System.out.println("进入方法,准备加锁");
synchronized (lockObj) {
instanceCount++;
System.out.println("代码块加锁,count=" + instanceCount);
}
}
}2.4 底层原理
核心基于对象头Mark Word和Monitor管程实现,JDK1.6+支持自动锁升级,升级路径不可逆:
无锁 → 偏向锁:无竞争场景,Mark Word记录首次访问的线程ID,后续该线程访问无需CAS加锁,消除无竞争开销;
偏向锁 → 轻量级锁(自适应自旋锁):多线程交替竞争锁时,线程通过CAS将Mark Word替换为栈帧中的锁记录指针,失败则自旋重试,避免线程进入操作系统级阻塞;
轻量级锁 → 重量级锁:自旋超过阈值仍未获取锁,升级为基于操作系统互斥量(Mutex)的重量级锁,未获取锁的线程进入阻塞状态,保证绝对的线程安全。
2.5 最佳实践
优先使用代码块锁,而非整个方法加锁,减小锁粒度与持有时间;
锁对象必须用final修饰,禁止用String、Integer等可变包装类,避免锁失效;
禁止在锁块中执行IO、网络请求等耗时操作;
避免锁嵌套,防止循环等待导致死锁。
3. ReentrantLock:JUC显式可重入锁
3.1 是什么
ReentrantLock 是JDK1.5引入的Lock接口核心实现类,基于AQS独占模式实现的显式、独占、可重入悲观锁,需要手动加锁与释放锁,提供了比synchronized更灵活的高级功能。
3.2 为什么用
解决synchronized的核心局限性,补充了业务开发必需的高级能力:
支持公平锁/非公平锁双模式(synchronized仅支持非公平锁);
支持可中断的锁获取
lockInterruptibly(),可打破死锁;支持超时获取锁
tryLock(),避免线程无限阻塞;支持多条件变量
Condition,可创建多个等待队列,实现精准的线程唤醒(synchronized仅支持一个wait/notify队列);支持查询锁状态:可判断锁是否被持有、当前线程是否持有锁、等待线程数等。
3.3 怎么用
核心铁则:unlock()必须放在finally块中,否则异常会导致锁永远无法释放,引发死锁。
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockDemo {
private int count = 0;
// 默认非公平锁,吞吐量更高
private final ReentrantLock nonFairLock = new ReentrantLock();
// 公平锁:严格FIFO顺序分配锁
private final ReentrantLock fairLock = new ReentrantLock(true);
// 多条件变量:实现精准唤醒
private final Condition oddCondition = nonFairLock.newCondition();
private final Condition evenCondition = nonFairLock.newCondition();
// 基础加锁释放
public void add() {
nonFairLock.lock();
try {
count++;
System.out.println("计数:" + count);
} finally {
nonFairLock.unlock();
}
}
// 超时获取锁:避免无限阻塞
public boolean tryAdd() {
try {
boolean getLock = nonFairLock.tryLock(2, TimeUnit.SECONDS);
if (!getLock) {
System.out.println("获取锁超时,放弃执行");
return false;
}
count++;
return true;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
} finally {
if (nonFairLock.isHeldByCurrentThread()) {
nonFairLock.unlock();
}
}
}
}3.4 底层原理
完全基于AQS实现:
用AQS的
state变量标记锁状态:state=0为无锁,state>0为有锁,数值代表重入次数;非公平锁:线程加锁时先直接CAS修改state从0→1,成功则获取锁,失败则进入CLH队列排队;
公平锁:线程加锁时先检查CLH队列是否有等待线程,有则直接排队,不抢锁;
可重入实现:加锁时判断当前持有锁的线程是否为自己,是则state+1,释放时state-1,直到state=0完全释放。
3.5 最佳实践
必须在finally块中释放锁,且释放前必须判断当前线程是否持有锁;
优先使用非公平锁,除非业务必须严格保证执行顺序;
高并发场景优先使用
tryLock()超时获取锁,替代无限阻塞的lock();多个Condition对应不同业务场景,实现精准唤醒,避免无效唤醒。
4. 读写锁体系:读多写少场景的性能优化方案
4.1 ReentrantReadWriteLock 可重入读写锁
是什么:实现了
ReadWriteLock接口,维护一对锁:读锁(共享锁,读读不互斥)、写锁(独占锁,读写/写写互斥),基于AQS实现,专门解决读多写少场景下独占锁的并发性能瓶颈。核心用法:
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockDemo {
private String cacheData;
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private final ReentrantReadWriteLock.ReadLock readLock = (ReentrantReadWriteLock.ReadLock) rwLock.readLock();
private final ReentrantReadWriteLock.WriteLock writeLock = (ReentrantReadWriteLock.WriteLock) rwLock.writeLock();
// 读操作:加共享读锁,多线程可并行执行
public String getData() {
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 读取数据:" + cacheData);
return cacheData;
} finally {
readLock.unlock();
}
}
// 写操作:加独占写锁,同一时间仅一个线程执行
public void setData(String newData) {
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 写入数据:" + newData);
this.cacheData = newData;
} finally {
writeLock.unlock();
}
}
}核心特性:支持锁降级(写锁→读锁),不支持锁升级(读锁→写锁,会导致死锁),存在写饥饿问题(读线程过多时写线程长期无法执行)。
最佳实践:仅在读操作远多于写操作的场景使用,避免长时间持有读锁,严格禁止锁升级。
4.2 StampedLock 邮戳锁
是什么:JDK1.8引入的新一代读写锁,基于自旋锁+CAS实现,通过
long类型的邮戳(stamp)标记锁状态,支持写锁、悲观读锁、乐观读三种模式,彻底解决写饥饿问题,性能远超ReentrantReadWriteLock。核心特性:乐观读模式无锁化,不阻塞写线程,性能极高;支持锁模式双向转换;不可重入,重复加锁会导致死锁;不支持Condition条件变量。
最佳实践:读多写少场景优先使用乐观读模式,严格避免重入加锁,释放锁必须使用加锁时返回的对应邮戳。
5. 锁核心特性术语全解
6. 锁的避坑指南
死锁规避:固定加锁顺序、避免锁嵌套、使用超时获取锁、支持可中断锁;
锁失效避坑:锁对象可变、锁范围过小导致原子性不满足、静态方法用this加锁;
性能坑点:锁粒度过大、锁持有时间过长、循环内频繁加锁释放锁、读写场景用独占锁。
第二部分:Java常用并发工具类全解析(解决协作问题)
并发工具类的核心使命,是基于锁体系的底层能力,封装开箱即用的多线程协作能力,解决线程间的等待、唤醒、同步、编排、限流等复杂业务逻辑,避免开发者手动基于锁重复造轮子。
1. CountDownLatch:线程倒计时计数器
1.1 是什么
CountDownLatch 是基于AQS共享模式实现的同步辅助工具,通过一个计数器实现:初始值为需要等待的线程数,每个线程完成任务后计数器减1,计数器归0时,所有等待的线程继续执行。计数器只能使用一次,不可重置。
1.2 为什么用
解决「主线程需要等待多个子线程完成任务后再继续执行」的场景(如并行加载配置、数据预处理、接口依赖的多个服务并行调用),避免手动使用wait/notify的复杂性。
1.3 怎么用
核心方法:CountDownLatch(int count)(构造函数)、countDown()(计数器减1)、await()(阻塞直到计数器归0)。
import java.util.concurrent.CountDownLatch;
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(3);
// 启动3个并行任务线程
for (int i = 1; i <= 3; i++) {
final int threadNum = i;
new Thread(() -> {
try {
System.out.println("线程" + threadNum + " 正在执行任务...");
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
latch.countDown(); // 任务完成,计数器减1
}
}).start();
}
System.out.println("主线程等待所有子任务完成...");
latch.await(); // 阻塞,直到计数器为0
System.out.println("所有子任务完成,主线程继续执行!");
}
}1.4 与锁体系的核心关联
底层完全基于AQS共享模式实现:用AQS的
state变量作为计数器,初始值为传入的count;countDown()方法:本质是释放共享锁,通过CAS将state减1;await()方法:本质是获取共享锁,只有当state=0时,才能获取成功,否则线程进入AQS的等待队列阻塞;可以理解为:它是基于AQS共享锁封装的「一次性线程等待工具」,锁的互斥能力是它实现同步的核心前提。
1.5 最佳实践
确保
countDown()在finally块中调用,防止线程异常导致计数器无法归0,造成死锁;使用
await(long timeout, TimeUnit unit)设置超时时间,避免无限等待;计数器不可重置,需要重复使用请选择CyclicBarrier。
2. CyclicBarrier:循环屏障
2.1 是什么
CyclicBarrier 是基于ReentrantLock+Condition实现的可循环使用的同步屏障,让一组线程到达屏障点时阻塞,直到所有线程都到达后,屏障才会打开,所有线程继续执行,支持屏障打开后执行自定义任务。可重置后重复使用。
2.2 为什么用
适用于「多线程分阶段执行任务」的场景(如并行计算,每个线程计算一部分数据,所有线程计算完后汇总结果,再进入下一阶段计算),解决CountDownLatch无法重复使用的痛点。
2.3 怎么用
核心方法:CyclicBarrier(int parties, Runnable barrierAction)(构造函数)、await()(到达屏障点,阻塞等待)。
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierDemo {
public static void main(String[] args) {
// 3个线程参与,屏障打开时执行汇总任务
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("所有线程已到达屏障,开始汇总结果!");
});
for (int i = 1; i <= 3; i++) {
final int threadNum = i;
new Thread(() -> {
try {
System.out.println("线程" + threadNum + " 正在计算数据...");
Thread.sleep(threadNum * 1000);
System.out.println("线程" + threadNum + " 到达屏障,等待其他线程...");
barrier.await(); // 阻塞,直到所有线程到达
System.out.println("线程" + threadNum + " 继续执行下一阶段任务!");
} catch (Exception e) {
Thread.currentThread().interrupt();
}
}).start();
}
}
}2.4 与锁体系的核心关联
底层完全基于ReentrantLock(显式锁)+ Condition实现,是锁能力的直接封装应用;
内部维护了一个计数器,用ReentrantLock保证计数器修改的原子性,避免多线程同时修改导致的数据错乱;
线程调用
await()时,先加锁,然后计数器减1,若计数器未归0,则通过Condition的await()进入等待队列阻塞;若计数器归0,则执行自定义任务,通过Condition的signalAll()唤醒所有等待的线程,最后重置计数器,实现循环使用;可以理解为:它是「锁+条件变量」封装的「可循环的线程同步工具」,没有锁的原子性保证,就无法实现屏障计数的准确性。
2.5 最佳实践
异常处理:若一个线程在
await()时被中断,其他线程会抛出BrokenBarrierException,需妥善处理;屏障任务
barrierAction由最后一个到达的线程执行,应避免耗时操作;通过
reset()方法重置屏障,实现多轮次的线程同步。
3. Semaphore:信号量
3.1 是什么
Semaphore 是基于AQS共享模式实现的计数信号量,用于控制同时访问特定资源的线程数量(即“限流”)。它通过维护一组“许可证”实现:线程获取许可证后才能访问资源,访问完释放许可证。
3.2 为什么用
适用于「资源池控制」场景(如数据库连接池、接口限流、分布式限流),防止过多线程同时访问资源导致系统崩溃。
3.3 怎么用
核心方法:Semaphore(int permits, boolean fair)(构造函数)、acquire()(获取许可证)、release()(释放许可证)。
import java.util.concurrent.Semaphore;
public class SemaphoreDemo {
public static void main(String[] args) {
// 3个许可证(模拟3个数据库连接),公平模式
Semaphore semaphore = new Semaphore(3, true);
for (int i = 1; i <= 5; i++) {
final int threadNum = i;
new Thread(() -> {
try {
System.out.println("线程" + threadNum + " 尝试获取数据库连接...");
semaphore.acquire(); // 获取许可证
System.out.println("线程" + threadNum + " 成功获取连接,开始操作数据库...");
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
semaphore.release(); // 释放许可证
System.out.println("线程" + threadNum + " 释放连接!");
}
}).start();
}
}
}3.4 与锁体系的核心关联
底层完全基于AQS共享模式实现,和CountDownLatch是同一套底层机制;
用AQS的
state变量作为许可证数量,初始值为传入的permits;acquire()方法:本质是获取共享锁,通过CAS将state减1,若state<0,说明无可用许可证,线程进入AQS等待队列阻塞;release()方法:本质是释放共享锁,通过CAS将state加1,同时唤醒等待队列中的线程;特殊场景:当permits=1时,Semaphore就退化为一个排他锁(互斥锁),和ReentrantLock功能一致,进一步证明了它的底层就是锁机制。
3.5 最佳实践
确保
release()在finally块中调用,防止线程异常导致许可证泄漏;公平模式保证线程按请求顺序获取许可证,性能略低;非公平模式吞吐量更高,需根据场景选择;
禁止无限制释放许可证,避免state数值超出初始值,导致限流失效。
4. Exchanger:线程间数据交换器
4.1 是什么
Exchanger 是基于自旋锁+CAS实现的线程间数据交换工具,允许两个线程在到达同步点时交换数据。若第一个线程先到达,会自旋等待第二个线程到达,然后双方交换数据后继续执行。
4.2 为什么用
适用于「两个线程需要双向交换数据」的场景(如遗传算法中的染色体交换、双线程数据校对、生产者消费者双线程数据传递)。
4.3 怎么用
核心方法:V exchange(V x)(交换数据,返回对方线程发送的数据)。
import java.util.concurrent.Exchanger;
public class ExchangerDemo {
public static void main(String[] args) {
Exchanger<String> exchanger = new Exchanger<>();
// 线程1:发送数据A
new Thread(() -> {
try {
String data1 = "数据A";
System.out.println("线程1 准备交换数据:" + data1);
String received = exchanger.exchange(data1);
System.out.println("线程1 收到数据:" + received);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
// 线程2:发送数据B
new Thread(() -> {
try {
String data2 = "数据B";
System.out.println("线程2 准备交换数据:" + data2);
Thread.sleep(1000);
String received = exchanger.exchange(data2);
System.out.println("线程2 收到数据:" + received);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
}
}4.4 与锁体系的核心关联
底层基于自旋锁+CAS无锁机制实现,和锁的底层CAS乐观锁机制完全一致,避免了重量级锁的上下文切换开销;
内部维护了一个数据槽位,通过volatile保证数据的可见性,通过CAS保证槽位数据修改的原子性;
第一个线程到达时,通过CAS将数据放入槽位,然后自旋等待;第二个线程到达时,通过CAS交换槽位数据,然后唤醒第一个线程;
可以理解为:它是乐观锁机制在双线程数据交换场景的定制化封装,核心还是保证共享数据操作的原子性和可见性。
4.5 最佳实践
仅适用于两个线程交换数据,多线程场景需使用其他工具;
使用
exchange(V x, long timeout, TimeUnit unit)设置超时时间,避免无限等待。
5. CompletableFuture:异步编程利器
5.1 是什么
CompletableFuture 是Java 8引入的异步编程工具类,实现了Future和CompletionStage接口,基于ForkJoinPool线程池+volatile+CAS实现,支持链式调用、组合式异步任务、异常处理等功能,解决了传统Future阻塞获取结果的痛点。
5.2 为什么用
传统Future需要通过get()阻塞获取结果,无法实现“任务完成后自动执行下一步”。CompletableFuture支持非阻塞式异步编程,可以轻松实现任务的串行、并行、组合执行,大幅提升并发编程的效率和可读性。
5.3 怎么用
核心方法分类:创建异步任务(supplyAsync()/runAsync())、链式回调(thenApply()/thenAccept())、组合任务(thenCombine()/allOf()/anyOf())、异常处理(exceptionally()/handle())。
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
public class CompletableFutureDemo {
public static void main(String[] args) {
// 异步查询用户信息
CompletableFuture<String> userFuture = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(1);
System.out.println("查询用户信息完成");
return "用户ID: 1001";
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return "查询用户失败";
}
});
// 异步查询订单信息(并行执行)
CompletableFuture<String> orderFuture = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(2);
System.out.println("查询订单信息完成");
return "订单号: 2023001";
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return "查询订单失败";
}
});
// 组合两个任务结果,汇总后输出
CompletableFuture<Void> resultFuture = userFuture.thenCombine(orderFuture, (user, order) -> {
return user + " | " + order;
}).thenAccept(result -> {
System.out.println("最终汇总结果: " + result);
});
resultFuture.join();
}
}5.4 与锁体系的核心关联
底层基于volatile+CAS乐观锁机制保证任务状态修改的原子性,和锁的底层核心机制一致;
内部维护了一个volatile修饰的任务状态变量,通过CAS修改状态,避免多线程同时修改任务状态导致的错乱;
异步任务中操作共享变量时,仍需依赖锁体系保证线程安全,它解决的是任务编排问题,不解决竞态问题;
可以理解为:它是基于无锁CAS机制实现的异步任务编排工具,和锁体系是「互补协同」的关系,任务编排靠它,线程安全靠锁。
5.5 最佳实践
自定义线程池执行异步任务,避免使用默认的
ForkJoinPool.commonPool(),防止核心业务与通用任务竞争资源;必须处理异常,使用
exceptionally()或handle()捕获异常,避免任务失败后无响应;尽量使用非阻塞回调方法,避免使用
get()阻塞主线程。
6. 并发工具类基础对比表
第三部分:锁与并发工具类的核心关联(重点补充)
前面的内容已经在每个组件中补充了单点关联,这里从底层、功能、实现、通信、场景五个维度,系统拆解二者的深度绑定关系,形成完整的知识闭环。
1. 底层基石:AQS是二者的共同核心
AQS是连接锁体系和并发工具类的底层桥梁,二者共享同一套同步机制,只是基于AQS的不同模式做了定制化实现:
锁体系(JUC显式锁):基于AQS的独占模式实现,核心是保证同一时间只有一个线程能持有同步状态,解决互斥问题;
并发工具类(CountDownLatch、Semaphore):基于AQS的共享模式实现,核心是允许多个线程同时持有同步状态,解决多线程协同问题;
二者共用同一套CLH等待队列、同一套线程阻塞/唤醒机制、同一套state状态管理逻辑,只是对state的语义做了不同定义:锁中state代表重入次数,CountDownLatch中state代表计数器,Semaphore中state代表许可证数量。
2. 功能定位:互补共生,缺一不可
锁和并发工具类的功能边界清晰,是「基础与上层」「前提与延伸」的关系,谁也无法替代谁:
锁是所有并发操作的基础前提:没有锁保证共享资源的原子性、可见性、有序性,多线程协作就无从谈起。比如CyclicBarrier的计数器修改,如果没有锁保证原子性,多线程同时修改会导致计数错乱,屏障功能直接失效;
并发工具类是锁能力的高阶延伸:锁只能解决「互斥」这个基础问题,无法直接实现「多线程倒计时等待、循环屏障、限流、异步编排」等复杂协作逻辑。如果没有工具类,开发者需要手动基于锁的wait/notify、Condition实现这些逻辑,代码复杂度极高,极易出现死锁、唤醒丢失等问题。
3. 实现逻辑:工具类底层完全依赖锁体系
所有并发工具类,本质都是「锁/锁的底层能力」在特定协作场景的封装实现,没有锁体系,就没有这些开箱即用的工具类:
强依赖显式锁:CyclicBarrier完全基于ReentrantLock+Condition实现,锁是它的核心骨架;
强依赖AQS锁机制:CountDownLatch、Semaphore完全基于AQS共享锁实现,底层就是一套定制化的共享锁逻辑;
强依赖锁的底层CAS机制:Exchanger、CompletableFuture基于自旋锁+CAS实现,和锁的乐观锁机制完全同源;
甚至可以说:所有并发工具类,都是锁的“语法糖”,只是把开发者需要重复编写的协作逻辑,基于锁做了标准化封装。
4. 线程通信:统一的「等待-通知」机制
不管是锁体系,还是并发工具类,底层的线程通信机制完全统一,都是基于「等待-通知」模型,只是封装层级不同:
最底层:synchronized的
wait/notify,基于Monitor管程的等待队列实现;中间层:ReentrantLock的
Condition.await/signal,基于AQS的Condition等待队列实现;上层:并发工具类的
await/countDown/await,本质都是对底层await/signal的封装。比如CountDownLatch的await()就是等待state=0的通知,countDown()就是计数器归0时发出通知;CyclicBarrier的await()就是等待所有线程到达的通知。
5. 场景协同:开发中二者的标准组合使用范式
在实际业务开发中,锁和并发工具类几乎都是成对出现、协同使用的,典型的组合场景包括:
并行计算汇总场景:用CountDownLatch等待多个计算线程完成任务,每个计算线程内部用ReentrantLock保证共享结果变量的线程安全;
分阶段任务执行场景:用CyclicBarrier实现多线程分阶段同步,每个阶段的任务执行用读写锁保证共享配置数据的安全读取与修改;
接口限流场景:用Semaphore实现接口并发限流,用ReentrantLock保证限流统计数据的原子性修改;
异步任务编排场景:用CompletableFuture实现异步任务的链式编排,任务内部用synchronized/ReentrantLock保证共享资源的线程安全。
第四部分:全场景选型指南
1. 锁体系选型决策树
基础并发场景、无特殊功能需求、不想手动管理锁释放 → 优先选
synchronized;需要公平锁、可中断、超时获取、多Condition精准唤醒 → 选
ReentrantLock;读多写少场景,需要可重入、锁降级、Condition → 选
ReentrantReadWriteLock;读多写少场景,追求极致性能,无锁嵌套需求 → 选
StampedLock。
2. 并发工具类选型决策树
主线程等待一组一次性任务完成,无需重复使用 → 选
CountDownLatch;多线程分阶段执行任务,需要循环重复使用 → 选
CyclicBarrier;控制并发线程数量,实现资源池限流 → 选
Semaphore;两个线程双向交换数据 → 选
Exchanger;异步任务编排、非阻塞回调、多任务组合 → 选
CompletableFuture。
3. 组合选型核心原则
先保证线程安全,再考虑协作逻辑:先选好合适的锁解决竞态问题,再选工具类实现协作逻辑;
最小化原则:优先使用封装层级高的工具类,避免手动基于锁实现复杂协作逻辑,减少出错概率;
性能优先原则:读多写少场景优先用读写锁+CompletableFuture,高并发限流场景用Semaphore+非公平锁,一次性等待场景用CountDownLatch。
第五部分:综合实战场景题
场景1:多线程轮流对数字+1,加到1000
需求:启动3个线程,轮流对初始为0的数字+1,直到达到1000,线程1打印1,线程2打印2,线程3打印3,依此类推。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class TurnAddDemo {
private static final int MAX = 1000;
private static int num = 0;
private static final ReentrantLock lock = new ReentrantLock();
// 三个Condition分别对应三个线程,实现精准唤醒
private static final Condition condition1 = lock.newCondition();
private static final Condition condition2 = lock.newCondition();
private static final Condition condition3 = lock.newCondition();
// 当前应该执行的线程标记:1-线程1,2-线程2,3-线程3
private static int flag = 1;
public static void main(String[] args) {
new Thread(() -> printNum(1, condition1, condition2), "线程1").start();
new Thread(() -> printNum(2, condition2, condition3), "线程2").start();
new Thread(() -> printNum(3, condition3, condition1), "线程3").start();
}
private static void printNum(int threadFlag, Condition currentCondition, Condition nextCondition) {
while (true) {
lock.lock();
try {
// 不是当前线程轮次,等待
while (flag != threadFlag) {
if (num >= MAX) break;
currentCondition.await();
}
if (num >= MAX) {
// 唤醒所有线程,避免死锁
nextCondition.signal();
break;
}
num++;
System.out.println(Thread.currentThread().getName() + " 打印: " + num);
// 切换到下一个线程
flag = (threadFlag % 3) + 1;
// 唤醒下一个线程
nextCondition.signal();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
}
}
}
}场景2:锁与并发工具类协同实战:并行数据统计
需求:启动4个线程,并行统计1-1000、1001-2000、2001-3000、3001-4000四个区间的偶数个数,最终汇总所有区间的结果,输出总偶数个数。要求用CountDownLatch实现线程等待,用ReentrantLock保证汇总结果的线程安全。
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.ReentrantLock;
public class ParallelCountDemo {
// 汇总结果:总偶数个数
private static int totalEvenCount = 0;
private static final ReentrantLock lock = new ReentrantLock();
private static final CountDownLatch latch = new CountDownLatch(4);
// 统计区间内的偶数个数
private static void countEven(int start, int end) {
int count = 0;
for (int i = start; i <= end; i++) {
if (i % 2 == 0) {
count++;
}
}
// 加锁,保证汇总结果的原子性
lock.lock();
try {
totalEvenCount += count;
System.out.println(Thread.currentThread().getName() + " 统计完成,区间[" + start + "," + end + "]偶数个数:" + count);
} finally {
lock.unlock();
}
// 计数器减1
latch.countDown();
}
public static void main(String[] args) throws InterruptedException {
// 启动4个统计线程
new Thread(() -> countEven(1, 1000), "统计线程1").start();
new Thread(() -> countEven(1001, 2000), "统计线程2").start();
new Thread(() -> countEven(2001, 3000), "统计线程3").start();
new Thread(() -> countEven(3001, 4000), "统计线程4").start();
// 主线程等待所有统计完成
System.out.println("主线程等待所有统计线程完成...");
latch.await();
System.out.println("所有统计完成,1-4000总偶数个数:" + totalEvenCount);
}
}场景3:多线程交替打印奇偶数
需求:两个线程交替打印1-100,线程1打印奇数,线程2打印偶数,严格交替执行。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class OddEvenPrint {
private static final int MAX = 100;
private int num = 0;
private final ReentrantLock lock = new ReentrantLock();
private final Condition oddCondition = lock.newCondition();
private final Condition evenCondition = lock.newCondition();
public void printOdd() {
while (num < MAX) {
lock.lock();
try {
while (num % 2 == 0) {
oddCondition.await();
}
System.out.println("奇数线程打印:" + num);
num++;
evenCondition.signal();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
}
}
}
public void printEven() {
while (num < MAX) {
lock.lock();
try {
while (num % 2 == 1) {
evenCondition.await();
}
System.out.println("偶数线程打印:" + num);
num++;
oddCondition.signal();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
OddEvenPrint demo = new OddEvenPrint();
new Thread(demo::printOdd).start();
new Thread(demo::printEven).start();
}
}第六部分:进阶并发工具类补全
上文已讲解5个核心并发工具类,这里补全生产环境高频使用、与锁体系深度绑定的3个进阶工具,每一个都对应解决上文基础工具的核心痛点,且完全遵循「底层依赖锁体系、上层解决协作问题」的核心逻辑。
6.1 Phaser:阶段器(CountDownLatch+CyclicBarrier的功能超集)
6.1.1 是什么
Phaser 是JDK1.7引入的可动态调整参与线程数、支持多轮次阶段同步的进阶同步工具,底层基于AQS共享模式实现,完全兼容CountDownLatch的一次性等待、CyclicBarrier的循环屏障能力,同时解决了二者的核心痛点:
CountDownLatch:计数器固定,无法动态增减线程数,只能一次性使用;
CyclicBarrier:参与线程数固定,启动后无法修改,不支持分层多阶段任务。
6.1.2 为什么用
在复杂的并行任务场景中(如大数据并行计算、多批次任务调度、动态线程数的分阶段执行),基础工具无法满足「动态调整线程数、多阶段分层同步、任务中途注册/注销」的需求,Phaser提供了更灵活、更强大的同步能力。
6.1.3 怎么用
核心方法与核心概念:
parties(参与方):注册的线程数,支持动态注册/注销;
phase(阶段):当前同步的轮次,每完成一轮同步,阶段号自动+1;
核心方法:
Phaser(int parties):构造函数,指定初始参与线程数;register()/bulkRegister(int parties):动态注册单个/多个参与方;arrive():到达屏障点,计数器减1,不阻塞,继续执行;arriveAndAwaitAdvance():到达屏障点,阻塞等待所有参与方到达,进入下一阶段;arriveAndDeregister():到达屏障点,注销当前参与方,减少后续阶段的参与线程数;onAdvance(int phase, int registeredParties):阶段切换时的钩子函数,可自定义阶段任务,返回true则终止Phaser。
代码示例:4个线程分3个阶段执行任务,动态注销线程
import java.util.concurrent.Phaser;
public class PhaserDemo {
// 定义3个执行阶段
private static final int TOTAL_PHASE = 3;
public static void main(String[] args) {
// 初始注册4个参与线程+1个主线程,共5个
Phaser phaser = new Phaser(5) {
// 重写阶段切换钩子函数,每完成一个阶段触发一次
@Override
protected boolean onAdvance(int phase, int registeredParties) {
System.out.println("========== 第" + (phase+1) + "阶段执行完成,当前参与线程数:" + registeredParties + " ==========\n");
// 达到总阶段数,返回true终止Phaser
return phase >= TOTAL_PHASE - 1 || registeredParties == 0;
}
};
// 启动4个工作线程
for (int i = 1; i <= 4; i++) {
final int threadNum = i;
new Thread(() -> {
// 第一阶段:所有线程都参与
System.out.println("线程" + threadNum + " 完成第一阶段任务");
phaser.arriveAndAwaitAdvance(); // 等待所有线程完成第一阶段
// 第二阶段:线程4注销,不再参与后续阶段
if (threadNum == 4) {
System.out.println("线程" + threadNum + " 完成任务,注销退出");
phaser.arriveAndDeregister(); // 到达并注销
return;
}
System.out.println("线程" + threadNum + " 完成第二阶段任务");
phaser.arriveAndAwaitAdvance();
// 第三阶段:仅剩余3个线程参与
System.out.println("线程" + threadNum + " 完成第三阶段任务");
phaser.arriveAndAwaitAdvance();
}, "线程" + i).start();
}
// 主线程等待所有阶段完成
for (int i = 0; i < TOTAL_PHASE; i++) {
phaser.arriveAndAwaitAdvance();
}
System.out.println("所有阶段任务全部执行完成!");
}
}6.1.4 与核心体系的关联
底层依赖:完全基于AQS共享模式实现,和CountDownLatch、Semaphore共用同一套底层同步机制,用AQS的
state变量高16位存储阶段号、低16位存储参与线程数,实现双维度的状态管理;功能互补:是CountDownLatch和CyclicBarrier的超集,可完全替代二者的所有功能,同时解决了二者的固定线程数、不可动态调整的痛点;
与锁的协同:阶段内的共享资源操作,仍需依赖锁体系保证线程安全,Phaser仅负责阶段同步,不解决竞态问题。
6.1.5 最佳实践
复杂分阶段、动态线程数的并行任务,优先使用Phaser替代CountDownLatch和CyclicBarrier,减少代码复杂度;
重写
onAdvance()方法实现阶段汇总、异常处理、终止逻辑,避免在业务代码中重复编写阶段判断;线程退出时必须调用
arriveAndDeregister()注销,否则会导致Phaser一直等待,引发阻塞死锁;避免在单阶段任务中执行耗时操作,导致其他线程长时间阻塞。
6.2 ThreadLocal:线程封闭工具(锁的无锁替代方案)
6.2.1 是什么
ThreadLocal 是Java提供的线程封闭工具,核心实现「线程级别的变量隔离」:每个线程都有自己独立的变量副本,线程之间互不影响,从根源上避免了多线程对共享变量的竞争,是一种无锁化的线程安全解决方案。
6.2.2 为什么用
解决多线程场景下的两个核心问题:
避免锁的性能开销:对于每个线程独立使用的变量,无需加锁互斥,通过线程隔离实现线程安全,无CAS自旋、无上下文切换,性能拉满;
线程上下文透传:在整个线程执行链路中,无需层层传递参数(如用户信息、traceId、数据库连接),通过ThreadLocal实现全局访问,简化代码。
6.2.3 怎么用
核心方法:
ThreadLocal<T>():构造函数,创建一个ThreadLocal实例;set(T value):为当前线程设置变量副本;T get():获取当前线程的变量副本;remove():移除当前线程的变量副本,核心避坑方法;initialValue():初始化变量的默认值,重写后可实现线程首次get时的默认值返回。
代码示例:微服务全链路traceId透传
public class ThreadLocalDemo {
// 定义全局ThreadLocal,存储链路traceId
private static final ThreadLocal<String> TRACE_ID_HOLDER = new ThreadLocal<String>() {
// 重写初始化方法,默认值为空字符串
@Override
protected String initialValue() {
return "";
}
};
// 模拟业务方法1:无需入参传递traceId,直接从ThreadLocal获取
private static void serviceMethod1() {
System.out.println("serviceMethod1执行,当前链路traceId:" + TRACE_ID_HOLDER.get());
serviceMethod2();
}
// 模拟业务方法2:深层链路方法,同样可直接获取
private static void serviceMethod2() {
System.out.println("serviceMethod2执行,当前链路traceId:" + TRACE_ID_HOLDER.get());
}
public static void main(String[] args) {
// 模拟3个不同的请求线程,每个线程有独立的traceId
for (int i = 1; i <= 3; i++) {
final String traceId = "trace-" + System.currentTimeMillis() + "-" + i;
new Thread(() -> {
try {
// 线程入口设置traceId
TRACE_ID_HOLDER.set(traceId);
System.out.println(Thread.currentThread().getName() + " 设置traceId:" + traceId);
// 执行业务链路,无需传递traceId
serviceMethod1();
} finally {
// 核心:线程执行完毕,必须移除变量,避免内存泄漏
TRACE_ID_HOLDER.remove();
}
}, "请求线程-" + i).start();
}
}
}6.2.4 与核心体系的关联
锁的替代方案:它是一种「无锁线程安全方案」,和锁体系是互补关系:共享资源多线程竞争用锁,线程独立使用的变量用ThreadLocal,避免不必要的锁开销;
底层实现:依赖
Thread类中的ThreadLocalMap实现,每个Thread线程都有自己独立的ThreadLocalMap,key为ThreadLocal实例的弱引用,value为变量副本;通过volatile保证Map的可见性,通过线程隔离保证原子性,无需加锁;与并发工具类的协同:在线程池、CompletableFuture等异步场景中,ThreadLocal无法自动透传,需配合
InheritableThreadLocal或TransmittableThreadLocal实现父子线程的变量传递,是异步任务编排场景的核心配套工具。
6.2.5 核心痛点与最佳实践
核心痛点:内存泄漏
原因:ThreadLocalMap的key是ThreadLocal的弱引用,GC时会被回收;但value是强引用,只要线程不终止(如线程池核心线程),value永远不会被回收,导致内存泄漏;
彻底解决方案:必须在finally块中调用
remove()方法,手动移除变量,这是唯一可靠的方案。
最佳实践:
ThreadLocal实例必须定义为
private static final,避免频繁创建ThreadLocal实例,导致Map中存在大量无效key;禁止使用ThreadLocal存储大对象,避免占用过多内存;
线程池场景下,必须在任务执行完毕后调用
remove(),否则线程复用会导致变量错乱;父子线程透传场景,优先使用阿里开源的
TransmittableThreadLocal,替代JDK自带的InheritableThreadLocal,解决线程池场景下的透传失效问题。
6.3 LockSupport:线程阻塞唤醒底层工具(锁体系的基石)
6.3.1 是什么
LockSupport 是JDK提供的线程阻塞与唤醒的底层工具类,是整个JUC锁体系、并发工具类的底层基石,所有的锁等待、线程阻塞逻辑,底层都是基于LockSupport实现的。它基于Unsafe类实现,核心提供两个方法:park()阻塞当前线程、unpark(Thread thread)唤醒指定线程。
6.3.2 为什么用
传统的wait/notify、Condition.await/signal存在两个核心痛点:
必须在同步代码块/锁块中执行,否则会抛出
IllegalMonitorStateException;唤醒方法必须在等待方法之后执行,先唤醒再等待会导致线程永久阻塞。
LockSupport彻底解决了这两个问题:
无需加锁,可在任意位置阻塞/唤醒线程;
支持「先唤醒、后阻塞」,不会导致线程永久阻塞,基于「许可」机制实现。
6.3.3 怎么用
核心方法:
park():阻塞当前线程,直到被unpark()唤醒、线程被中断、超时时间到达;park(Object blocker):阻塞当前线程,记录阻塞对象,便于排查死锁问题,推荐优先使用;parkNanos(long nanos):阻塞当前线程,最长阻塞指定纳秒数;parkUntil(long deadline):阻塞当前线程,直到指定的时间戳;unpark(Thread thread):给指定线程发放许可,唤醒处于阻塞状态的线程;若线程未阻塞,后续调用park()时会直接放行,不会阻塞。
代码示例:两个线程的精准唤醒,无需加锁
import java.util.concurrent.locks.LockSupport;
public class LockSupportDemo {
public static void main(String[] args) {
Thread threadA = new Thread(() -> {
System.out.println("线程A启动,准备阻塞");
// 阻塞当前线程,无需加锁
LockSupport.park();
System.out.println("线程A被唤醒,继续执行");
}, "线程A");
Thread threadB = new Thread(() -> {
System.out.println("线程B启动,准备唤醒线程A");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// 唤醒线程A,先unpark再park也不会失效
LockSupport.unpark(threadA);
System.out.println("线程B完成唤醒操作");
}, "线程B");
// 先启动线程A,再启动线程B
threadA.start();
threadB.start();
// 测试:先unpark再park,依然有效
Thread testThread = new Thread(() -> {
try {
Thread.sleep(1000);
System.out.println("测试线程准备调用park");
LockSupport.park(); // 先被unpark,这里直接放行,不会阻塞
System.out.println("测试线程park直接放行,执行完成");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "测试线程");
testThread.start();
// 先发放许可,后续park直接放行
LockSupport.unpark(testThread);
System.out.println("主线程先调用了unpark,测试线程后续park不会阻塞");
}
}6.3.4 与核心体系的关联
锁体系的底层基石:ReentrantLock、读写锁、AQS的线程阻塞与唤醒,底层完全基于LockSupport实现。AQS中线程获取锁失败后,调用
LockSupport.park()进入阻塞;锁释放时,调用LockSupport.unpark()唤醒队列中的线程;Condition的底层实现:
Condition.await()/signal()的底层,就是LockSupport的park()/unpark();并发工具类的底层依赖:CyclicBarrier、Phaser等工具的线程等待逻辑,底层全部基于LockSupport实现;
与wait/notify的对比:
6.3.5 最佳实践
优先使用
park(Object blocker)方法,传入this作为blocker,便于jstack排查死锁时定位阻塞对象;park()方法会响应线程中断,但不会抛出中断异常,需要手动判断Thread.interrupted()状态处理中断;避免在循环外调用
park(),建议在循环中判断业务条件,防止虚假唤醒;仅用于底层框架开发,业务代码优先使用更高封装层级的锁、并发工具类,避免直接使用LockSupport。
第七部分:AQS深度源码级原理解析
上文多次提到,AQS是整个JUC锁体系和并发工具类的共同底层基石,这里深入源码级解析AQS的核心设计、数据结构、执行流程,彻底搞懂Java并发的底层核心。
7.1 AQS核心设计思想
AQS全称AbstractQueuedSynchronizer,抽象队列同步器,核心设计思想是:用一个volatile修饰的int类型state变量表示同步状态,通过内置的FIFO双向队列(CLH队列)管理获取同步状态失败的线程,将线程封装为Node节点放入队列阻塞,同步状态释放时唤醒队列头部的线程重试获取同步状态。
它采用了模板方法设计模式,将通用的同步队列管理、线程阻塞唤醒逻辑封装在父类AQS中,子类只需重写以下5个核心方法,实现自定义的同步逻辑,无需关心底层队列管理:
tryAcquire(int arg):独占式获取同步状态,成功返回true,失败返回false;tryRelease(int arg):独占式释放同步状态,成功返回true,失败返回false;tryAcquireShared(int arg):共享式获取同步状态,返回值>=0表示成功,<0表示失败;tryReleaseShared(int arg):共享式释放同步状态,成功返回true,失败返回false;isHeldExclusively():判断当前线程是否持有独占同步状态。
7.2 AQS核心数据结构
7.2.1 核心状态变量:volatile int state
private volatile int state;用volatile修饰,保证多线程之间的可见性和有序性;
语义由子类自定义:
ReentrantLock:state=0表示无锁,state>0表示有锁,数值代表重入次数;
CountDownLatch:state表示剩余计数器值,state=0表示所有任务完成;
Semaphore:state表示剩余许可证数量;
ReentrantReadWriteLock:state高16位表示读锁持有次数,低16位表示写锁重入次数。
7.2.2 CLH双向FIFO等待队列
AQS的等待队列是一个双向链表结构,每个节点封装了等待的线程、等待状态、前驱和后继节点,核心结构如下:
static final class Node {
// 节点模式:共享模式、独占模式
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
// 节点等待状态
static final int CANCELLED = 1; // 线程已取消,不再竞争同步状态
static final int SIGNAL = -1; // 后继节点处于等待状态,当前节点释放同步状态时需要唤醒后继
static final int CONDITION = -2; // 节点处于Condition等待队列中
static final int PROPAGATE = -3; // 共享模式下,唤醒操作需要向后传播
volatile int waitStatus; // 节点等待状态
volatile Node prev; // 前驱节点
volatile Node next; // 后继节点
volatile Thread thread; // 封装的等待线程
Node nextWaiter; // Condition等待队列的下一个节点
}
// 队列头节点(哑节点,不存储等待线程)
private transient volatile Node head;
// 队列尾节点
private transient volatile Node tail;队列特性:
头节点是哑节点,不存储等待线程,仅作为队列的标记,真正等待的线程从第二个节点开始;
新的等待线程会被封装为Node节点,通过CAS加入队列尾部,保证线程安全;
同步状态释放时,会唤醒头节点的后继节点,被唤醒的线程重试获取同步状态,成功则将自己设置为新的头节点。
7.2.3 Condition等待队列
每个Condition对象对应一个单向的等待队列,用于实现锁的等待/唤醒机制,和AQS的主等待队列是分离的:
线程调用
Condition.await()时,会释放锁,将自己封装为Node节点加入Condition等待队列阻塞;线程调用
Condition.signal()时,会将等待队列的头节点移动到AQS主等待队列中,等待被唤醒获取锁。
7.3 独占模式核心流程(对应ReentrantLock)
独占模式的核心是:同一时间只能有一个线程持有同步状态,对应ReentrantLock的加锁/释放锁逻辑。
7.3.1 加锁流程(acquire方法)
public final void acquire(int arg) {
// 1. 尝试获取同步状态,成功则直接返回
if (!tryAcquire(arg) &&
// 2. 获取失败,将线程封装为Node节点加入等待队列尾部
// 3. 阻塞等待,被唤醒后重试获取,直到成功
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 4. 获取过程中被中断,设置线程中断标记
selfInterrupt();
}完整执行步骤:
调用子类重写的
tryAcquire()方法,尝试获取同步状态,成功则直接返回,无需进入队列;获取失败,调用
addWaiter()方法,将当前线程封装为独占模式的Node节点,通过CAS加入等待队列尾部;调用
acquireQueued()方法,节点进入队列后,自旋判断前驱节点是否是头节点:若是头节点,再次尝试获取同步状态,成功则将自己设置为新的头节点,返回;
若不是头节点,或获取失败,调用
LockSupport.park()阻塞当前线程,等待被前驱节点唤醒;
线程被唤醒后,重复步骤3,直到成功获取同步状态;
若线程在阻塞过程中被中断,会设置中断标记,返回后调用
selfInterrupt()补全中断状态。
7.3.2 释放锁流程(release方法)
public final boolean release(int arg) {
// 1. 尝试释放同步状态
if (tryRelease(arg)) {
Node h = head;
// 2. 头节点不为空,且等待状态为SIGNAL,需要唤醒后继节点
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}完整执行步骤:
调用子类重写的
tryRelease()方法,尝试释放同步状态,失败则直接返回false;释放成功,获取队列头节点,判断是否需要唤醒后继节点;
调用
unparkSuccessor()方法,通过LockSupport.unpark()唤醒头节点的后继节点;被唤醒的线程回到
acquireQueued()方法,重试获取同步状态。
7.3.3 公平锁与非公平锁的核心区别
非公平锁:
tryAcquire()方法中,不管队列中是否有等待线程,直接先CAS修改state尝试获取锁,抢不到再进入队列;公平锁:
tryAcquire()方法中,先调用hasQueuedPredecessors()判断队列中是否有等待的线程,有则直接返回false,不抢锁,进入队列排队,严格遵循FIFO顺序。
7.4 共享模式核心流程(对应CountDownLatch、Semaphore)
共享模式的核心是:同一时间可以有多个线程持有同步状态,对应CountDownLatch、Semaphore的核心逻辑。
7.4.1 获取同步状态流程(acquireShared方法)
public final void acquireShared(int arg) {
// 1. 尝试获取共享同步状态,返回值>=0表示成功
if (tryAcquireShared(arg) < 0)
// 2. 获取失败,进入队列阻塞等待
doAcquireShared(arg);
}完整执行步骤:
调用子类重写的
tryAcquireShared()方法,尝试获取共享同步状态,返回值>=0表示成功,直接返回;获取失败,调用
doAcquireShared()方法,将线程封装为共享模式的Node节点加入队列尾部;自旋判断前驱节点是否是头节点,若是则再次尝试获取同步状态:
成功则设置新的头节点,并向后传播唤醒操作,唤醒后续共享模式的节点;
失败则阻塞等待,被唤醒后重复重试。
7.4.2 释放同步状态流程(releaseShared方法)
public final boolean releaseShared(int arg) {
// 1. 尝试释放共享同步状态
if (tryReleaseShared(arg)) {
// 2. 释放成功,唤醒队列中的等待节点
doReleaseShared();
return true;
}
return false;
}完整执行步骤:
调用子类重写的
tryReleaseShared()方法,尝试释放共享同步状态,失败则返回false;释放成功,调用
doReleaseShared()方法,唤醒头节点的后继节点;共享模式下,唤醒操作会向后传播,所有符合条件的共享节点都会被依次唤醒,这是和独占模式的核心区别。
以CountDownLatch为例:
tryAcquireShared():判断state是否为0,是则返回1(成功),否则返回-1(失败);tryReleaseShared():通过CAS将state减1,当state减到0时返回true,触发唤醒操作;这就是CountDownLatch的
await()和countDown()的底层实现。
第八部分:生产级最佳实践与避坑指南
8.1 线程池与并发工具类的协同规范
CompletableFuture必须自定义线程池:禁止使用默认的
ForkJoinPool.commonPool(),该池是JVM全局共享的,核心线程数为CPU核心数,若有IO密集型任务会导致整个JVM的异步任务阻塞,必须根据业务类型自定义线程池:CPU密集型任务:核心线程数=CPU核心数+1;
IO密集型任务:核心线程数=CPU核心数*2;
线程池与CountDownLatch/CyclicBarrier配合时,避免线程耗尽:若任务之间有依赖关系,所有任务必须使用不同的线程池,否则会出现「核心任务占用所有线程,等待任务阻塞,核心任务无法执行」的死锁问题;
ThreadLocal在线程池场景必须手动remove:线程池的线程是复用的,若任务执行完毕不调用
remove(),会导致变量错乱、内存泄漏,最佳实践是通过AOP/拦截器统一处理set和remove。
8.2 CompletableFuture生产级避坑
必须处理所有异常:异步任务中抛出的异常,若没有用
exceptionally()/handle()捕获,会被静默吞掉,不会打印日志,导致问题无法排查;必须设置超时时间:所有异步任务必须通过
orTimeout()/completeOnTimeout()设置超时时间,避免远程调用阻塞导致异步任务永远无法完成;禁止在回调方法中执行耗时操作:
thenApply()/thenAccept()等回调方法会在异步任务的执行线程中运行,耗时操作会占用线程池的核心线程,导致线程池阻塞;区分
thenRun()和thenRunAsync():不带Async的回调方法会复用执行异步任务的线程,带Async的会重新提交到线程池执行,避免线程复用导致的上下文错乱。
8.3 锁体系的生产级调优
高并发场景优先减小锁粒度:
锁分段:将一个大锁拆分为多个小锁,如ConcurrentHashMap的分段思想,不同的资源用不同的锁;
锁细化:只对核心的原子操作加锁,非原子操作移出锁块,减小锁持有时间;
读多写少场景优先用StampedLock乐观读:替代ReentrantReadWriteLock,避免写饥饿问题,同时乐观读无锁化,性能提升显著;
高竞争场景关闭偏向锁:JDK1.8默认开启偏向锁,但高并发多线程竞争场景下,偏向锁的撤销开销远大于收益,可通过JVM参数
-XX:-UseBiasedLocking关闭偏向锁,减少性能损耗;避免锁粗化过度:循环内的操作,禁止将锁加在循环内部,应将锁移到循环外,避免频繁加锁释放锁的开销。
8.4 死锁的生产级排查与解决方案
8.4.1 快速排查命令
# 1. 查看Java进程PID
jps
# 2. 查看线程堆栈,自动检测死锁
jstack <PID>jstack会直接在输出末尾标注Found one Java-level deadlock:,并给出死锁的线程、持有的锁、等待的锁,直接定位问题。
8.4.2 生产级解决方案
固定加锁顺序:所有线程必须按照统一的全局顺序加锁,比如按锁对象的hashCode升序加锁,彻底打破循环等待条件;
使用超时锁:所有加锁操作使用
tryLock(long timeout, TimeUnit unit)设置超时时间,超时则释放已持有的锁,打破请求与保持条件;使用可中断锁:使用
lockInterruptibly()加锁,支持线程中断,打破不可剥夺条件;避免锁嵌套:业务代码中尽量减少锁的嵌套使用,从根源上避免死锁。