本文完整合并「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锁可按核心特性划分为以下类别,所有分类都围绕「解决竞态问题的不同策略」展开:

划分维度

核心分类

实现层级

JVM内置锁(synchronized)、JUC显式锁(Lock接口体系)

可重入性

可重入锁、不可重入锁

公平性

公平锁、非公平锁

资源访问权限

排他锁(独占锁)、共享锁

加锁策略

悲观锁、乐观锁

阻塞方式

自旋锁、阻塞锁

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 WordMonitor管程实现,JDK1.6+支持自动锁升级,升级路径不可逆:

  1. 无锁 → 偏向锁:无竞争场景,Mark Word记录首次访问的线程ID,后续该线程访问无需CAS加锁,消除无竞争开销;

  2. 偏向锁 → 轻量级锁(自适应自旋锁):多线程交替竞争锁时,线程通过CAS将Mark Word替换为栈帧中的锁记录指针,失败则自旋重试,避免线程进入操作系统级阻塞;

  3. 轻量级锁 → 重量级锁:自旋超过阈值仍未获取锁,升级为基于操作系统互斥量(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. 锁核心特性术语全解

特性分类

核心定义与区别

可重入锁 vs 不可重入锁

可重入锁:同一线程可多次获取同一把锁,不会自己阻塞自己;不可重入锁反之,重复加锁会直接死锁

公平锁 vs 非公平锁

公平锁:严格FIFO顺序分配锁,无饥饿但吞吐量低;非公平锁:允许抢锁,吞吐量高但可能出现饥饿

排他锁 vs 共享锁

排他锁:同一时间仅一个线程持有,所有操作互斥;共享锁:同一时间可多个线程持有,读读不互斥

悲观锁 vs 乐观锁

悲观锁:默认竞争一定会发生,操作前先加锁;乐观锁:默认竞争不会发生,提交时校验冲突,无锁化

自旋锁 vs 阻塞锁

自旋锁:获取锁失败时循环重试,不释放CPU,无上下文切换;阻塞锁:获取失败立即阻塞,释放CPU

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引入的异步编程工具类,实现了FutureCompletionStage接口,基于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. 并发工具类基础对比表

工具类

核心功能

可重置性

主要使用场景

底层依赖的锁机制

CountDownLatch

计数器,等待一组线程完成

并行任务初始化、主线程等待子线程

AQS共享锁模式

CyclicBarrier

屏障,线程互相等待到齐

多阶段并行计算、分阶段任务同步

ReentrantLock + Condition

Semaphore

信号量,控制并发线程数

资源池控制、接口限流

AQS共享锁模式

Exchanger

两个线程间交换数据

数据校对、双线程数据交换

自旋锁 + CAS乐观锁

CompletableFuture

异步编程,任务链式组合

-

非阻塞异步任务、任务编排

volatile + CAS乐观锁 + 线程池


第三部分:锁与并发工具类的核心关联(重点补充)

前面的内容已经在每个组件中补充了单点关联,这里从底层、功能、实现、通信、场景五个维度,系统拆解二者的深度绑定关系,形成完整的知识闭环。

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. 场景协同:开发中二者的标准组合使用范式

在实际业务开发中,锁和并发工具类几乎都是成对出现、协同使用的,典型的组合场景包括:

  1. 并行计算汇总场景:用CountDownLatch等待多个计算线程完成任务,每个计算线程内部用ReentrantLock保证共享结果变量的线程安全;

  2. 分阶段任务执行场景:用CyclicBarrier实现多线程分阶段同步,每个阶段的任务执行用读写锁保证共享配置数据的安全读取与修改;

  3. 接口限流场景:用Semaphore实现接口并发限流,用ReentrantLock保证限流统计数据的原子性修改;

  4. 异步任务编排场景:用CompletableFuture实现异步任务的链式编排,任务内部用synchronized/ReentrantLock保证共享资源的线程安全。


第四部分:全场景选型指南

1. 锁体系选型决策树

  1. 基础并发场景、无特殊功能需求、不想手动管理锁释放 → 优先选synchronized

  2. 需要公平锁、可中断、超时获取、多Condition精准唤醒 → 选ReentrantLock

  3. 读多写少场景,需要可重入、锁降级、Condition → 选ReentrantReadWriteLock

  4. 读多写少场景,追求极致性能,无锁嵌套需求 → 选StampedLock

2. 并发工具类选型决策树

  1. 主线程等待一组一次性任务完成,无需重复使用 → 选CountDownLatch

  2. 多线程分阶段执行任务,需要循环重复使用 → 选CyclicBarrier

  3. 控制并发线程数量,实现资源池限流 → 选Semaphore

  4. 两个线程双向交换数据 → 选Exchanger

  5. 异步任务编排、非阻塞回调、多任务组合 → 选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 与核心体系的关联

  1. 底层依赖:完全基于AQS共享模式实现,和CountDownLatch、Semaphore共用同一套底层同步机制,用AQS的state变量高16位存储阶段号、低16位存储参与线程数,实现双维度的状态管理;

  2. 功能互补:是CountDownLatch和CyclicBarrier的超集,可完全替代二者的所有功能,同时解决了二者的固定线程数、不可动态调整的痛点;

  3. 与锁的协同:阶段内的共享资源操作,仍需依赖锁体系保证线程安全,Phaser仅负责阶段同步,不解决竞态问题。

6.1.5 最佳实践

  • 复杂分阶段、动态线程数的并行任务,优先使用Phaser替代CountDownLatch和CyclicBarrier,减少代码复杂度;

  • 重写onAdvance()方法实现阶段汇总、异常处理、终止逻辑,避免在业务代码中重复编写阶段判断;

  • 线程退出时必须调用arriveAndDeregister()注销,否则会导致Phaser一直等待,引发阻塞死锁;

  • 避免在单阶段任务中执行耗时操作,导致其他线程长时间阻塞。

6.2 ThreadLocal:线程封闭工具(锁的无锁替代方案)

6.2.1 是什么

ThreadLocal 是Java提供的线程封闭工具,核心实现「线程级别的变量隔离」:每个线程都有自己独立的变量副本,线程之间互不影响,从根源上避免了多线程对共享变量的竞争,是一种无锁化的线程安全解决方案

6.2.2 为什么用

解决多线程场景下的两个核心问题:

  1. 避免锁的性能开销:对于每个线程独立使用的变量,无需加锁互斥,通过线程隔离实现线程安全,无CAS自旋、无上下文切换,性能拉满;

  2. 线程上下文透传:在整个线程执行链路中,无需层层传递参数(如用户信息、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 与核心体系的关联

  1. 锁的替代方案:它是一种「无锁线程安全方案」,和锁体系是互补关系:共享资源多线程竞争用锁,线程独立使用的变量用ThreadLocal,避免不必要的锁开销;

  2. 底层实现:依赖Thread类中的ThreadLocalMap实现,每个Thread线程都有自己独立的ThreadLocalMap,key为ThreadLocal实例的弱引用,value为变量副本;通过volatile保证Map的可见性,通过线程隔离保证原子性,无需加锁;

  3. 与并发工具类的协同:在线程池、CompletableFuture等异步场景中,ThreadLocal无法自动透传,需配合InheritableThreadLocalTransmittableThreadLocal实现父子线程的变量传递,是异步任务编排场景的核心配套工具。

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/notifyCondition.await/signal存在两个核心痛点:

  1. 必须在同步代码块/锁块中执行,否则会抛出IllegalMonitorStateException

  2. 唤醒方法必须在等待方法之后执行,先唤醒再等待会导致线程永久阻塞。

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 与核心体系的关联

  1. 锁体系的底层基石:ReentrantLock、读写锁、AQS的线程阻塞与唤醒,底层完全基于LockSupport实现。AQS中线程获取锁失败后,调用LockSupport.park()进入阻塞;锁释放时,调用LockSupport.unpark()唤醒队列中的线程;

  2. Condition的底层实现Condition.await()/signal()的底层,就是LockSupport的park()/unpark()

  3. 并发工具类的底层依赖:CyclicBarrier、Phaser等工具的线程等待逻辑,底层全部基于LockSupport实现;

  4. 与wait/notify的对比

特性

LockSupport

wait/notify

Condition.await/signal

加锁要求

无需加锁,任意位置可调用

必须在synchronized块中

必须在Lock锁块中

唤醒顺序

先unpark后park依然有效

先notify后wait永久阻塞

先signal后await永久阻塞

唤醒精度

可精准唤醒指定线程

只能随机唤醒一个/全部

可精准唤醒对应等待队列

中断响应

响应中断,不抛出异常

响应中断,抛出InterruptedException

同wait

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();
}

完整执行步骤:

  1. 调用子类重写的tryAcquire()方法,尝试获取同步状态,成功则直接返回,无需进入队列;

  2. 获取失败,调用addWaiter()方法,将当前线程封装为独占模式的Node节点,通过CAS加入等待队列尾部;

  3. 调用acquireQueued()方法,节点进入队列后,自旋判断前驱节点是否是头节点:

    1. 若是头节点,再次尝试获取同步状态,成功则将自己设置为新的头节点,返回;

    2. 若不是头节点,或获取失败,调用LockSupport.park()阻塞当前线程,等待被前驱节点唤醒;

  4. 线程被唤醒后,重复步骤3,直到成功获取同步状态;

  5. 若线程在阻塞过程中被中断,会设置中断标记,返回后调用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;
}

完整执行步骤:

  1. 调用子类重写的tryRelease()方法,尝试释放同步状态,失败则直接返回false;

  2. 释放成功,获取队列头节点,判断是否需要唤醒后继节点;

  3. 调用unparkSuccessor()方法,通过LockSupport.unpark()唤醒头节点的后继节点;

  4. 被唤醒的线程回到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);
}

完整执行步骤:

  1. 调用子类重写的tryAcquireShared()方法,尝试获取共享同步状态,返回值>=0表示成功,直接返回;

  2. 获取失败,调用doAcquireShared()方法,将线程封装为共享模式的Node节点加入队列尾部;

  3. 自旋判断前驱节点是否是头节点,若是则再次尝试获取同步状态:

    1. 成功则设置新的头节点,并向后传播唤醒操作,唤醒后续共享模式的节点;

    2. 失败则阻塞等待,被唤醒后重复重试。

7.4.2 释放同步状态流程(releaseShared方法)

public final boolean releaseShared(int arg) {
    // 1. 尝试释放共享同步状态
    if (tryReleaseShared(arg)) {
        // 2. 释放成功,唤醒队列中的等待节点
        doReleaseShared();
        return true;
    }
    return false;
}

完整执行步骤:

  1. 调用子类重写的tryReleaseShared()方法,尝试释放共享同步状态,失败则返回false;

  2. 释放成功,调用doReleaseShared()方法,唤醒头节点的后继节点;

  3. 共享模式下,唤醒操作会向后传播,所有符合条件的共享节点都会被依次唤醒,这是和独占模式的核心区别。

以CountDownLatch为例:

  • tryAcquireShared():判断state是否为0,是则返回1(成功),否则返回-1(失败);

  • tryReleaseShared():通过CAS将state减1,当state减到0时返回true,触发唤醒操作;

  • 这就是CountDownLatch的await()countDown()的底层实现。


第八部分:生产级最佳实践与避坑指南

8.1 线程池与并发工具类的协同规范

  1. CompletableFuture必须自定义线程池:禁止使用默认的ForkJoinPool.commonPool(),该池是JVM全局共享的,核心线程数为CPU核心数,若有IO密集型任务会导致整个JVM的异步任务阻塞,必须根据业务类型自定义线程池:

    1. CPU密集型任务:核心线程数=CPU核心数+1;

    2. IO密集型任务:核心线程数=CPU核心数*2;

  2. 线程池与CountDownLatch/CyclicBarrier配合时,避免线程耗尽:若任务之间有依赖关系,所有任务必须使用不同的线程池,否则会出现「核心任务占用所有线程,等待任务阻塞,核心任务无法执行」的死锁问题;

  3. ThreadLocal在线程池场景必须手动remove:线程池的线程是复用的,若任务执行完毕不调用remove(),会导致变量错乱、内存泄漏,最佳实践是通过AOP/拦截器统一处理set和remove。

8.2 CompletableFuture生产级避坑

  1. 必须处理所有异常:异步任务中抛出的异常,若没有用exceptionally()/handle()捕获,会被静默吞掉,不会打印日志,导致问题无法排查;

  2. 必须设置超时时间:所有异步任务必须通过orTimeout()/completeOnTimeout()设置超时时间,避免远程调用阻塞导致异步任务永远无法完成;

  3. 禁止在回调方法中执行耗时操作thenApply()/thenAccept()等回调方法会在异步任务的执行线程中运行,耗时操作会占用线程池的核心线程,导致线程池阻塞;

  4. 区分thenRun()thenRunAsync():不带Async的回调方法会复用执行异步任务的线程,带Async的会重新提交到线程池执行,避免线程复用导致的上下文错乱。

8.3 锁体系的生产级调优

  1. 高并发场景优先减小锁粒度

    1. 锁分段:将一个大锁拆分为多个小锁,如ConcurrentHashMap的分段思想,不同的资源用不同的锁;

    2. 锁细化:只对核心的原子操作加锁,非原子操作移出锁块,减小锁持有时间;

  2. 读多写少场景优先用StampedLock乐观读:替代ReentrantReadWriteLock,避免写饥饿问题,同时乐观读无锁化,性能提升显著;

  3. 高竞争场景关闭偏向锁:JDK1.8默认开启偏向锁,但高并发多线程竞争场景下,偏向锁的撤销开销远大于收益,可通过JVM参数-XX:-UseBiasedLocking关闭偏向锁,减少性能损耗;

  4. 避免锁粗化过度:循环内的操作,禁止将锁加在循环内部,应将锁移到循环外,避免频繁加锁释放锁的开销。

8.4 死锁的生产级排查与解决方案

8.4.1 快速排查命令

# 1. 查看Java进程PID
jps
# 2. 查看线程堆栈,自动检测死锁
jstack <PID>

jstack会直接在输出末尾标注Found one Java-level deadlock:,并给出死锁的线程、持有的锁、等待的锁,直接定位问题。

8.4.2 生产级解决方案

  1. 固定加锁顺序:所有线程必须按照统一的全局顺序加锁,比如按锁对象的hashCode升序加锁,彻底打破循环等待条件;

  2. 使用超时锁:所有加锁操作使用tryLock(long timeout, TimeUnit unit)设置超时时间,超时则释放已持有的锁,打破请求与保持条件;

  3. 使用可中断锁:使用lockInterruptibly()加锁,支持线程中断,打破不可剥夺条件;

  4. 避免锁嵌套:业务代码中尽量减少锁的嵌套使用,从根源上避免死锁。


两块二每分钟