【JUC合集-01】JUC并发编程复习总结

目录

JUC(java.util.concurrent)并发编程总结

[一、核心认知:JUC 到底是什么](#一、核心认知:JUC 到底是什么)

[1.1 本质定位](#1.1 本质定位)

[1.2 JUC 整体体系](#1.2 JUC 整体体系)

二、原子类:无锁原子操作

[2.1 核心原理:CAS](#2.1 核心原理:CAS)

[2.2 常用原子类](#2.2 常用原子类)

[2.3 AtomicInteger 实战与源码解析](#2.3 AtomicInteger 实战与源码解析)

[2.4 LongAdder 为什么更快](#2.4 LongAdder 为什么更快)

[三、锁体系:比 synchronized 更灵活](#三、锁体系:比 synchronized 更灵活)

[3.1 Lock 接口总览](#3.1 Lock 接口总览)

[3.2 ReentrantLock 重入锁](#3.2 ReentrantLock 重入锁)

尝试获取锁:tryLock

[公平锁 vs 非公平锁](#公平锁 vs 非公平锁)

[3.3 ReentrantReadWriteLock 读写锁](#3.3 ReentrantReadWriteLock 读写锁)

[3.4 ReentrantLock 和 synchronized 对比](#3.4 ReentrantLock 和 synchronized 对比)

四、三大常用同步工具类

[4.1 CountDownLatch:倒计时门闩](#4.1 CountDownLatch:倒计时门闩)

[4.2 CyclicBarrier:循环栅栏](#4.2 CyclicBarrier:循环栅栏)

[4.3 CountDownLatch vs CyclicBarrier](#4.3 CountDownLatch vs CyclicBarrier)

[4.4 Semaphore:信号量](#4.4 Semaphore:信号量)

五、线程池:线程复用管理

[5.1 为什么要用线程池](#5.1 为什么要用线程池)

[5.2 ThreadPoolExecutor 七大核心参数](#5.2 ThreadPoolExecutor 七大核心参数)

[5.3 线程池执行流程](#5.3 线程池执行流程)

[5.4 四种拒绝策略](#5.4 四种拒绝策略)

[5.5 Executors 自带线程池为什么不推荐](#5.5 Executors 自带线程池为什么不推荐)

[5.6 自定义线程池最佳实践](#5.6 自定义线程池最佳实践)

六、并发容器

[6.1 ConcurrentHashMap](#6.1 ConcurrentHashMap)

演进历程

关键特性

[6.2 CopyOnWriteArrayList](#6.2 CopyOnWriteArrayList)

[6.3 其他并发容器](#6.3 其他并发容器)

[七、阻塞队列 BlockingQueue](#七、阻塞队列 BlockingQueue)

[7.1 核心特性](#7.1 核心特性)

[7.2 四组操作方法](#7.2 四组操作方法)

[7.3 常用实现类对比](#7.3 常用实现类对比)

[八、底层基石:AQS 抽象队列同步器](#八、底层基石:AQS 抽象队列同步器)

[8.1 什么是 AQS](#8.1 什么是 AQS)

[8.2 核心思想](#8.2 核心思想)

[8.3 两种模式](#8.3 两种模式)

[8.4 为什么 AQS 这么重要](#8.4 为什么 AQS 这么重要)

九、踩坑与实践

[1. 锁相关踩坑](#1. 锁相关踩坑)

[2. 线程池踩坑](#2. 线程池踩坑)

[3. 并发容器踩坑](#3. 并发容器踩坑)

[4. 通用最佳实践](#4. 通用最佳实践)

十、复习速记


JUC(java.util.concurrent)并发编程总结

基于 JDK 1.8 标准 API 整理,覆盖核心工具、底层原理、实战用法与踩坑

一、核心认知:JUC 到底是什么

1.1 本质定位

JUC 就是 java.util.concurrent 包的简称,是 JDK 官方提供的并发编程工具集 。在 JUC 出现之前,Java 做并发只能靠 synchronizedwait()notify() 这些底层原语,写起来麻烦、功能单一、容易出错。JUC 把并发开发中常用的锁、原子类、线程池、同步工具、并发容器、阻塞队列等能力全部封装好,让开发者不用自己从零实现并发控制,大大降低了并发编程的门槛。

它解决的核心痛点:

  • synchronized 是独占锁,不支持读写分离、不支持尝试获取锁、不支持中断等待
  • 手动写 wait/notify 很容易写错,导致假唤醒、死锁
  • 没有统一的线程池管理,手动创建线程容易失控
  • 没有现成的并发容器,手动写线程安全集合容易出问题
  • 没有同步工具类,多线程协作逻辑要自己反复造轮子

1.2 JUC 整体体系

整个 JUC 包可以分成六大核心模块:

  1. 原子类java.util.concurrent.atomic 包下,基于 CAS 实现无锁原子操作
  2. 锁体系locks 包下,Lock 接口、重入锁、读写锁等,基于 AQS 实现
  3. 同步工具CountDownLatchCyclicBarrierSemaphore 等,用于多线程协作
  4. 线程池Executor 体系,统一管理线程生命周期,复用线程
  5. 并发容器ConcurrentHashMapCopyOnWriteArrayList 等线程安全集合
  6. 阻塞队列BlockingQueue 体系,用于生产者消费者场景、线程池任务队列

二、原子类:无锁原子操作

2.1 核心原理:CAS

原子类能实现线程安全,核心靠 CAS(Compare And Swap,比较并交换),这是一种无锁并发方案。

  • 执行更新操作时,先拿到旧值,真正写入前比较当前内存值是不是和旧值一致
  • 一致就把新值写进去,返回成功
  • 不一致就说明中间被别的线程改过了,返回失败,一般会自旋重试

CAS 是 CPU 级别的原子指令,全程没有加锁,性能远高于锁,特别适合并发量不极端高的热点数据更新。

ABA 问题 :CAS 只看值有没有变,不看值的变化过程。如果一个值从 A 改成 B,又改回 A,CAS 会认为它没变过,但实际已经被修改过。解决办法是加版本号,每次修改版本号加一,比较的时候连版本号一起比,JDK 提供了 AtomicStampedReference 就是干这个的。

2.2 常用原子类

类型 代表类 作用
基本类型 AtomicInteger、AtomicLong、AtomicBoolean 原子更新基本类型
引用类型 AtomicReference、AtomicStampedReference 原子更新对象引用,带版本号解决 ABA
数组类型 AtomicIntegerArray、AtomicLongArray 原子更新数组里的元素
对象属性更新 AtomicIntegerFieldUpdater 原子更新对象的某个字段,不用改原类
累加器 LongAdder、DoubleAdder 高并发下累加性能比 AtomicLong 好,适合统计计数

2.3 AtomicInteger 实战与源码解析

最常用的原子类,用于原子计数,比如接口访问量统计、分布式序号生成。

java 复制代码
public class AtomicIntegerDemo {
    // 初始值为0的原子整型
    private static final AtomicInteger count = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        // 10个线程,每个加1000次
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    // 原子自增1,等价于count++,但线程安全
                    count.incrementAndGet();
                }
            }).start();
        }

        Thread.sleep(2000);
        // 最终结果一定是10000,不会出现并发丢失更新的问题
        System.out.println(count.get());
    }
}

底层源码拆解(incrementAndGet)

java 复制代码
// Unsafe是CAS的核心类,直接操作内存,所有原子类都靠它
public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

// Unsafe里的核心方法,自旋+CAS
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        // 从内存中拿到当前值
        var5 = this.getIntVolatile(var1, var2);
        // CAS尝试更新,成功就返回旧值,失败就循环重试
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    return var5;
}

本质就是自旋 CAS:拿值 → 尝试更新 → 失败就再拿再试,直到成功为止。并发不高的时候很快,冲突特别激烈的时候,大量线程自旋会浪费 CPU。

2.4 LongAdder 为什么更快

高并发下大量线程同时 CAS 一个变量,冲突严重,很多线程空转重试,性能会下降。 LongAdder 做了优化:把一个值拆成多个 Cell 单元格,每个线程去更新自己对应的 Cell,最后求和的时候把所有 Cell 加起来。这样大大降低了冲突概率,并发越高优势越明显。 代价是数据有短暂的弱一致性,求和不一定完全精准,适合统计计数、监控指标这种不需要绝对精确的场景。

三、锁体系:比 synchronized 更灵活

3.1 Lock 接口总览

Lock 是 JUC 锁的顶层接口,最常用的实现是 ReentrantLock(重入锁)和 ReentrantReadWriteLock(读写锁)。

synchronized 相比,Lock 的优势:

  • 可以尝试获取锁,拿不到就返回,不会一直死等
  • 可以响应中断,等待锁的过程中可以被打断
  • 支持公平锁和非公平锁切换
  • 支持读写分离,读多写少场景性能更好
  • 可以绑定多个条件变量,精确唤醒不同的线程

3.2 ReentrantLock 重入锁

和 synchronized 一样,都是可重入的:同一个线程可以多次获取同一把锁,不会自己把自己锁住。

基础用法

java 复制代码
public class ReentrantLockDemo {
    // 创建重入锁,默认是非公平锁
    private final Lock lock = new ReentrantLock();
    private int count = 0;

    public void add() {
        // 加锁,和synchronized进入代码块等价
        lock.lock();
        try {
            // 业务代码放try里
            count++;
        } finally {
            // 必须在finally里手动释放锁,不然异常了锁就泄露了
            lock.unlock();
        }
    }
}

注意:Lock 必须手动解锁,而且一定要写在 finally 里,不然代码抛异常了锁永远不会释放,别的线程永远拿不到,最终死锁。这是最容易犯的错。

尝试获取锁:tryLock

这是最实用的特性,避免线程无限阻塞:

java 复制代码
public boolean tryAdd() {
    // 尝试获取锁,等3秒还拿不到就放弃,返回false
    try {
        if (lock.tryLock(3, TimeUnit.SECONDS)) {
            try {
                count++;
                return true;
            } finally {
                lock.unlock();
            }
        }
    } catch (InterruptedException e) {
        // 等待过程中被中断的处理
        Thread.currentThread().interrupt();
    }
    // 拿锁失败,返回失败,业务可以做降级处理
    return false;
}
公平锁 vs 非公平锁
  • 非公平锁(默认):来抢锁的线程不管队列里有没有人在等,直接先尝试抢,抢不到再去排队。优点是吞吐量高,缺点是可能出现线程饥饿,有的线程一直抢不到。
  • 公平锁:严格按先来后到排队,谁等得久谁先拿。优点是公平,不会饥饿;缺点是吞吐量低,大量线程切换开销大。
java 复制代码
// true就是公平锁,false非公平
Lock fairLock = new ReentrantLock(true);

3.3 ReentrantReadWriteLock 读写锁

现实中很多场景是读多写少,普通锁不管读写都独占,读的时候也不能并发读,浪费性能。 读写锁把锁分成读锁和写锁:

  • 读锁:共享锁,多个线程可以同时持有读锁,并发读不互斥
  • 写锁:独占锁,写的时候不能有任何读或写
  • 规则:读 - 读不互斥,读 - 写互斥,写 - 写互斥
java 复制代码
public class ReadWriteLockDemo {
    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    private final Lock readLock = rwLock.readLock();
    private final Lock writeLock = rwLock.writeLock();
    private String data;

    // 读操作,加读锁,多线程可以同时读
    public String read() {
        readLock.lock();
        try {
            return data;
        } finally {
            readLock.unlock();
        }
    }

    // 写操作,加写锁,同一时间只能一个写
    public void write(String newData) {
        writeLock.lock();
        try {
            this.data = newData;
        } finally {
            writeLock.unlock();
        }
    }
}

适用场景:读多写少,比如配置缓存、元数据管理,读的频率远高于写,用读写锁性能提升非常明显。

3.4 ReentrantLock 和 synchronized 对比

对比项 ReentrantLock synchronized
实现层面 JDK 层面实现的 API JVM 层面关键字,底层靠 monitor
锁释放 必须手动 unlock,一般放 finally 自动释放,代码块结束或异常都自动释放
锁特性 支持尝试获取、可中断、公平 / 非公平切换 非公平,不可中断,不能尝试
锁类型 独占锁,也有读写锁扩展 独占锁,只有一种
条件变量 支持多个 Condition,精确唤醒 只有一个 waitSet,只能全部唤醒
性能 高并发下更好,功能灵活 低并发下够用,JDK1.6 后优化了很多,性能差距不大
易用性 容易写错,忘记释放锁会死锁 简单,JVM 兜底

总结:普通同步场景用 synchronized 就够了,简单不容易错;需要高级特性(尝试锁、可中断、公平锁、读写锁)的时候再用 ReentrantLock。

四、三大常用同步工具类

4.1 CountDownLatch:倒计时门闩

作用:一个线程等待多个线程完成任务后,自己再继续执行。相当于发令枪:所有人都到终点了,裁判才宣布比赛结束。

  • 只能用一次,计数减到 0 就不能再用了
  • 调用 countDown() 计数减 1,调用 await() 阻塞等待计数归零

代码示例:等待所有子任务完成再汇总

java 复制代码
public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        // 初始化计数器,数值为3,代表要等3个线程完成
        CountDownLatch latch = new CountDownLatch(3);

        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                try {
                    System.out.println(Thread.currentThread().getName() + " 执行任务");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    // 任务完成,计数减1,必须放finally保证一定执行
                    latch.countDown();
                }
            }).start();
        }

        System.out.println("主线程等待所有子任务完成...");
        // 主线程阻塞,直到计数变成0
        latch.await();
        System.out.println("所有任务完成,主线程继续执行");
    }
}

典型场景:并行计算,多个线程分别算不同部分,全部算完主线程汇总结果;启动时等待多个资源初始化完成再对外提供服务。

4.2 CyclicBarrier:循环栅栏

作用:多个线程互相等待,所有线程都到达栅栏位置后,再一起继续执行。可以循环使用,重置后能反复用。

  • 每个线程调用 await() 就到达栅栏,阻塞等待
  • 最后一个线程到达后,所有线程同时被唤醒,继续执行
  • 可以设置一个栅栏动作,最后一个线程到达时自动执行

代码示例:运动员到齐了一起起跑

java 复制代码
public class CyclicBarrierDemo {
    public static void main(String[] args) {
        // 等待3个线程,全部到齐后执行后面的Runnable
        CyclicBarrier barrier = new CyclicBarrier(3, () -> {
            System.out.println("所有人都到齐了,预备,跑!");
        });

        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                try {
                    System.out.println(Thread.currentThread().getName() + " 到达起跑线");
                    // 到达栅栏,等待其他人
                    barrier.await();
                    System.out.println(Thread.currentThread().getName() + " 开始跑");
                } catch (InterruptedException | BrokenBarrierException e) {
                    Thread.currentThread().interrupt();
                }
            }).start();
        }
    }
}

4.3 CountDownLatch vs CyclicBarrier

区别 CountDownLatch CyclicBarrier
等待关系 一个线程等多个线程 多个线程互相等
能否复用 只能用一次,计数到 0 就废了 可以循环使用,重置后反复用
计数方式 调用 countDown 减 1 调用 await 计数加 1,到阈值释放
额外动作 没有内置回调 支持设置栅栏动作,最后一个线程到达时执行
异常处理 中断抛 InterruptedException 栅栏破了抛 BrokenBarrierException

4.4 Semaphore:信号量

作用:控制同时访问某个资源的线程数量,相当于限流。有 N 个许可,线程拿一个许可才能执行,执行完归还许可,许可用完了其他线程就阻塞等。

代码示例:限流器,最多 3 个线程同时执行

java 复制代码
public class SemaphoreDemo {
    public static void main(String[] args) {
        // 初始化3个许可,最多3个线程同时执行
        Semaphore semaphore = new Semaphore(3);

        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    // 获取一个许可,拿不到就阻塞
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + " 获得许可,开始执行");
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    // 释放许可
                    semaphore.release();
                    System.out.println(Thread.currentThread().getName() + " 释放许可");
                }
            }).start();
        }
    }
}

典型场景:接口限流、数据库连接池控制、资源访问并发数控制。

五、线程池:线程复用管理

5.1 为什么要用线程池

手动 new Thread 弊端很多:

  • 每次创建销毁线程开销大,频繁创建销毁性能差
  • 线程数量不受控,高并发下创建几百上千个线程,内存和 CPU 都扛不住
  • 不好管理,没法统一监控、统计、调优

线程池的核心思想就是线程复用:提前创建好一批线程,任务来了直接用线程执行,执行完不销毁,继续等下一个任务。既降低了创建销毁开销,又能控制并发数量。

5.2 ThreadPoolExecutor 七大核心参数

这是面试必考点,也是自定义线程池的基础。

java 复制代码
public ThreadPoolExecutor(
    int corePoolSize,          // 1. 核心线程数
    int maximumPoolSize,       // 2. 最大线程数
    long keepAliveTime,        // 3. 空闲线程存活时间
    TimeUnit unit,             // 4. 存活时间单位
    BlockingQueue<Runnable> workQueue,  // 5. 任务队列
    ThreadFactory threadFactory,        // 6. 线程工厂
    RejectedExecutionHandler handler    // 7. 拒绝策略
)

逐个解释:

  1. 核心线程数:线程池长期保留的线程数,即使空闲也不会销毁,除非设置了允许核心线程超时。
  2. 最大线程数:线程池最多能创建的线程总数,核心线程 + 非核心线程。
  3. 空闲存活时间:非核心线程空闲超过这个时间就会被销毁。核心线程默认不回收。
  4. 时间单位:配合存活时间使用,秒、毫秒等。
  5. 任务队列:核心线程都忙的时候,新任务放到队列里等待。
  6. 线程工厂:创建线程的工厂,可以自定义线程名、是否守护线程、优先级等,方便排查问题。
  7. 拒绝策略:线程和队列都满了,新任务来的时候怎么处理。

5.3 线程池执行流程

新任务提交到线程池,执行顺序是:

记忆顺序:核心线程 → 队列 → 非核心线程 → 拒绝策略。很多人会搞错,以为先开非核心线程再进队列,实际是先进队列,队列满了才开非核心。

5.4 四种拒绝策略

策略 行为 适用场景
AbortPolicy(默认) 直接抛出 RejectedExecutionException 异常 大多数场景,异常及时暴露
CallerRunsPolicy 由提交任务的主线程自己执行这个任务 不能丢任务,流量平稳的场景
DiscardPolicy 直接丢掉任务,什么都不做 不重要的任务,丢了也无所谓
DiscardOldestPolicy 丢掉队列里最老的任务,把新任务加进去 最新任务最重要的场景

5.5 Executors 自带线程池为什么不推荐

JDK 提供了 Executors 工具类快速创建线程池,但《阿里巴巴开发规范》明确禁止用它创建线程池,原因:

  1. FixedThreadPool 和 SingleThreadPool :队列用的是无界的 LinkedBlockingQueue,任务多了会无限堆积,内存溢出。
  2. CachedThreadPool:最大线程数是 Integer.MAX_VALUE,高并发下会创建无数线程,系统卡死。
  3. ScheduledThreadPool:同样是无界队列,容易 OOM。

生产环境必须用 ThreadPoolExecutor 手动指定参数,根据业务场景设置合理的核心线程数、队列大小、最大线程数、拒绝策略。

5.6 自定义线程池最佳实践

java 复制代码
public class ThreadPoolUtil {
    private static final ThreadPoolExecutor EXECUTOR;

    static {
        // 核心线程数根据业务来:CPU密集型设为CPU核数,IO密集型设为2*CPU核数
        int coreSize = Runtime.getRuntime().availableProcessors() * 2;
        EXECUTOR = new ThreadPoolExecutor(
                coreSize,
                coreSize * 2,
                60,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(500),
                // 自定义线程工厂,给线程起名字,排查问题方便
                new ThreadFactory() {
                    private final AtomicInteger threadNum = new AtomicInteger(1);
                    @Override
                    public Thread newThread(Runnable r) {
                        return new Thread(r, "biz-pool-" + threadNum.getAndIncrement());
                    }
                },
                // 自定义拒绝策略,记录日志+降级处理
                new ThreadPoolExecutor.CallerRunsPolicy()
        );
        // 允许核心线程超时回收,长期空闲可以释放资源
        EXECUTOR.allowCoreThreadTimeOut(true);
    }

    public static void execute(Runnable task) {
        EXECUTOR.execute(task);
    }
}

六、并发容器

6.1 ConcurrentHashMap

线程安全的 HashMap,是并发场景下最常用的集合。

演进历程
  • JDK 1.7:分段锁(Segment),把整个 Map 分成 16 段,每段一把锁,不同段之间可以并发写。缺点是分段粒度还是大,同一段内还是串行,并发度最高 16。
  • JDK 1.8 :废弃分段锁,改用 CAS + synchronized,锁的粒度是每个数组元素(每个桶)。没有哈希冲突的时候用 CAS 放元素,有冲突或者扩容的时候才加 synchronized 锁。粒度更细,并发性能更好,同时结构和普通 HashMap 保持一致,都是数组 + 链表 + 红黑树。
关键特性
  • 不允许 null 键和 null 值,和 HashMap 不一样
  • 迭代器是弱一致性的,遍历过程中数据修改不会抛并发修改异常,但遍历的是某个时间点的快照,不一定能看到最新修改
  • size 计算是估算值,不是绝对精确,并发场景下没有绝对准确的 size

6.2 CopyOnWriteArrayList

写时复制的 ArrayList,核心思想:

  • 读操作完全不加锁,直接读原数组,性能很高
  • 写操作的时候,复制一份新数组,在新数组上修改,改完把数组引用指向新数组
  • 写的时候加锁,防止并发写复制多份

优点:读性能极高,完全无锁,适合读多写极少的场景

缺点:写的时候要复制数组,内存开销大;数据有延迟,写的内容不能立刻被读到,弱一致性

适用场景:配置列表、黑白名单、订阅关系,读非常多,写非常少,允许短暂不一致的场景。

6.3 其他并发容器

  • ConcurrentSkipListMap:线程安全的跳表实现,有序,对应 TreeMap 的并发版
  • ConcurrentLinkedQueue:无界非阻塞并发队列,CAS 实现,高并发性能好
  • BlockingQueue:阻塞队列,下面单独讲

七、阻塞队列 BlockingQueue

7.1 核心特性

阻塞队列是特殊的队列,支持阻塞式的插入和移除:

  • 队列满的时候,插入元素的线程会阻塞,直到队列有空位
  • 队列空的时候,取出元素的线程会阻塞,直到队列有元素

天生适合生产者消费者模式,也是线程池任务队列的核心。

7.2 四组操作方法

操作方式 抛出异常 返回特殊值 阻塞等待 超时等待
插入 add(e) offer(e) put(e) offer(e, time, unit)
移除 remove() poll() take() poll(time, unit)
检查队首 element() peek() - -

7.3 常用实现类对比

实现类 底层结构 是否有界 特点 适用场景
ArrayBlockingQueue 数组 有界,创建时必须指定大小 固定容量,公平 / 非公平模式 固定容量的生产者消费者、线程池队列
LinkedBlockingQueue 链表 默认无界,也可指定容量 吞吐量比数组版高,头尾两把锁 大多数阻塞场景,FixedThreadPool 用的就是它
SynchronousQueue 不存储元素,直接传递 容量为 0 来了任务必须有线程接,不然就阻塞 CachedThreadPool 用的就是它,直接递交给线程
DelayQueue 优先级队列 无界 元素必须实现 Delayed 接口,到时间才能取出 定时任务、延迟消息、超时订单关闭
PriorityBlockingQueue 优先级队列 无界 按优先级出队,不是先进先出 需要按优先级处理任务的场景

八、底层基石:AQS 抽象队列同步器

8.1 什么是 AQS

AQS 全称 AbstractQueuedSynchronizer,是 JUC 里几乎所有锁和同步工具的底层框架。ReentrantLockCountDownLatchSemaphoreReentrantReadWriteLock 全都是基于 AQS 实现的。

它把并发同步的通用逻辑全部抽出来了:

  • 用一个 volatile int state 变量表示同步状态
  • 用一个 CLH 变体的双向链表做等待队列,抢不到锁的线程排到队列里
  • 提供了独占模式和共享模式两种同步方式
  • 子类只需要实现几个简单的方法,就能轻松实现自己的同步器

8.2 核心思想

抢锁失败的线程,不会一直自旋浪费 CPU,而是封装成节点放到等待队列里挂起,锁释放的时候再唤醒队列里的下一个线程。

队列是双向链表,每个节点保存线程引用、等待状态、前驱后继指针。

  • 入队:新节点加到队尾,用 CAS 保证原子性
  • 出队:头节点释放锁后,唤醒下一个节点的线程去抢锁

8.3 两种模式

  1. 独占模式 :同一时间只能有一个线程持有锁,比如 ReentrantLock。代表方法 acquire()release()
  2. 共享模式 :多个线程可以同时持有锁,比如 Semaphore、读写锁的读锁。代表方法 acquireShared()releaseShared()

8.4 为什么 AQS 这么重要

它是 JUC 的骨架,把所有同步器通用的「排队、阻塞、唤醒、状态管理」逻辑都封装好了,上层工具不用重复实现。理解了 AQS,再看各种锁和同步工具,本质都是在操作 state 变量:

  • ReentrantLock:state=0 表示锁空闲,state>0 表示被占用,重入一次加 1
  • CountDownLatch:state 初始是计数,countDown 一次减 1,减到 0 就释放所有等待线程
  • Semaphore:state 是剩余许可数,acquire 减 1,release 加 1

九、踩坑与实践

1. 锁相关踩坑

  • ReentrantLock 忘记 unlock:必须放 finally,不然异常了锁泄露,最终死锁
  • 锁重入次数不匹配:加了几次锁就要解几次锁,多解会抛异常,少解锁不释放
  • 读写锁写锁饥饿:读特别多的时候,写锁一直抢不到,会饥饿。可以用公平模式缓解
  • synchronized 锁范围太大 / 太小:太大并发差,太小不安全,刚好包住共享变量操作就行

2. 线程池踩坑

  • 用 Executors 创建线程池:无界队列 / 无线程上限,生产环境必炸,一定要手动 new ThreadPoolExecutor
  • 核心线程数设置不合理:CPU 密集型(计算多)设为 CPU 核数;IO 密集型(数据库、网络调用多)设为 2*CPU 核数,当然洛需要根据压测调整,毕竟只是理论数据,还得看实际情况
  • 线程池不关闭:程序退出线程池还在,导致进程关不掉。优雅停机要调用 shutdown ()
  • 任务异常吞掉:execute 提交的任务抛异常会导致线程销毁,最好自己在任务里 try-catch 处理

3. 并发容器踩坑

  • ConcurrentHashMap 不是万能的:单个操作是原子的,但多步组合操作不是,比如先判断存在再更新,这两步之间可能被别的线程改,需要自己加锁或者用 compute 等原子方法
  • CopyOnWrite 写太多性能差:写一次复制一次数组,写多了内存和性能都扛不住,只适合读多写极少
  • 遍历集合时修改:并发容器不会抛并发修改异常,但不代表就可以随便边遍历边改,结果是不可预期的

4. 通用最佳实践

  • 优先用工具类,不要自己手写 wait/notify,很容易写错
  • 优先降低锁粒度,能锁代码块就别锁整个方法,能锁局部变量就别锁整个对象
  • 能无锁就不用锁,原子类能解决的就别用锁
  • 并发场景下优先考虑最终一致,不要强求强一致,性能差很多
  • 给线程起有意义的名字,排查问题的时候一眼就能认出是哪个池的线程
  • 避免死锁:固定加锁顺序,避免循环依赖;设置锁超时时间;尽量不要嵌套加锁

十、复习速记

  1. JUC 六大模块:原子类、锁、同步工具、线程池、并发容器、阻塞队列。
  2. 原子类核心:CAS + 自旋,CPU 级原子指令,无锁并发;高并发计数用 LongAdder 性能更好。
  3. 锁体系:ReentrantLock 灵活可中断、支持尝试锁;读写锁适合读多写少;synchronized 简单不易错,普通场景够用。
  4. 三大工具:CountDownLatch 一等多,一次性;CyclicBarrier 多等一,可复用;Semaphore 限并发数量。
  5. 线程池七大参数:核心数、最大数、存活时间、单位、队列、线程工厂、拒绝策略;执行顺序:核心→队列→非核心→拒绝。
  6. ConcurrentHashMap:1.8 用 CAS + synchronized,桶粒度锁,数组 + 链表 + 红黑树,比 1.7 分段锁性能好。
  7. AQS 基石:state 状态变量 + CLH 等待队列,独占 / 共享两种模式,是所有锁和同步工具的底层。
  8. 核心原则:优先用 JDK 现成工具,不重复造轮子;锁粒度尽量小;线程池必须自定义参数。
相关推荐
Tisfy6 小时前
LeetCode 1979.找出数组的最大公约数:模拟(附手动gcd)
java·数学·算法·leetcode·题解·最大公约数
糖果店的幽灵6 小时前
【langgraph 从入门到精通graphApi 篇】节点与边 —— 图的骨架
java·数据库·人工智能·redis·langgraph
CHANG_THE_WORLD6 小时前
7.C++标准多线程实现 TCP通信
java·c++·tcp/ip
chen_ke_hao6 小时前
K8s 高可用集群部署
java·docker·kubernetes
CodeStats6 小时前
《源纹天书》第一百八十一章至第一百八十五章:跨界bug·双世界联合debug
java·源纹天书
小则又沐风a6 小时前
库的原理:目标文件,ELF格式,程序加载
java·linux·前端
我是唐青枫7 小时前
Java Neo4j 实战指南:从图模型、Cypher 到 Spring Boot 关系查询
java·spring boot·neo4j
Java搬码工16 小时前
CompletableFuture 完整详解 + 全场景使用案例
java
许心月16 小时前
Java学习资料网站汇总
java