
1. 项目概述为什么我们要死磕JUC如果你是一名Java开发者尤其是工作一两年后开始接触并发编程那么“JUC”这三个字母对你来说绝对是一个绕不开、又爱又恨的存在。爱它是因为它提供了Java并发编程的“核武器”能让你写出高性能、高并发的系统恨它是因为它的底层原理复杂概念抽象稍有不慎就会掉进各种“坑”里比如死锁、线程饥饿、数据竞争调试起来让人头皮发麻。这个项目就是一次对Java并发工具包java.util.concurrent简称JUC的“掘地三尺”式学习。它不仅仅是对API的简单罗列而是深入到每个核心类的源码层面结合操作系统原理、CPU架构和内存模型去理解为什么Doug LeaJUC的设计者要这样设计以及我们该如何正确地使用它。市面上很多教程只讲“怎么用”但这个项目追求的是“为什么这么用”以及“底层是怎么实现的”。我们会结合“狂神说Java”系列中关于JUC的经典笔记脉络并融入全网最新的实践案例、面试高频考点和性能调优经验打造一份既系统全面又深入透彻的学习指南。无论你是正在准备冲击大厂面试需要在并发问题上和面试官“掰掰手腕”还是在实际工作中遇到了性能瓶颈需要优化线程池、锁或者并发容器亦或是单纯对Java并发这座“高山”充满征服欲这份详细的学习笔记都能为你提供一条清晰的攀登路径。我们将从最基础的线程状态和创建方式开始逐步深入到AQS、锁、原子类、线程池、并发容器等核心领域确保每个环节都讲透原理、给足案例、指出陷阱。2. 核心基石从Java内存模型JMM与并发理论基础讲起在直接上手ReentrantLock或ThreadPoolExecutor之前我们必须先打好地基。这个地基就是Java内存模型JMM和并发的三大核心问题可见性、原子性、有序性。很多并发Bug的根源都在于此理解了它们再看JUC的各种工具就会有一种“降维打击”的感觉。2.1 重新认识“变量”可见性问题与volatile关键字一个最经典的入门问题启动两个线程一个修改变量flag为true另一个循环读取flag为什么读取线程可能永远看不到修改后的值// 一个典型的可见性问题示例 public class VisibilityDemo { private static /*volatile*/ boolean flag false; // 试试去掉volatile public static void main(String[] args) throws InterruptedException { new Thread(() - { System.out.println(线程A开始执行...); try { Thread.sleep(1000); // 模拟业务耗时 } catch (InterruptedException e) { e.printStackTrace(); } flag true; // 1. 修改共享变量 System.out.println(线程A将flag设置为true); }, Thread-A).start(); new Thread(() - { System.out.println(线程B开始循环检测...); while (!flag) { // 2. 循环读取共享变量 // 空循环可能永远跳不出来 } System.out.println(线程B检测到flag变化退出循环); }, Thread-B).start(); } }运行上面代码去掉volatile你很可能会发现线程B一直卡在while循环里即使线程A早已将flag改为了true。这就是可见性问题。底层原理与volatile的作用 现代CPU为了提升性能普遍采用了多级缓存结构L1, L2, L3。每个线程运行时可能会将共享变量从主内存加载到自己的CPU缓存中进行操作。线程A修改了缓存中的flag值但这个修改可能并没有立即写回主内存。同时线程B读取flag时是从自己的CPU缓存中读取的旧值这就导致了数据不一致。volatile关键字的作用就是解决这个问题。它有两层语义保证可见性当一个线程修改了volatile变量的值这个新值会立即被强制刷新到主内存。而当其他线程需要读取这个变量时它会去主内存中读取新值而不是使用自己缓存中的旧值。禁止指令重排序编译器或处理器为了优化性能可能会对指令进行重排序。volatile通过插入内存屏障Memory Barrier来禁止这种重排序保证了有序性。实操心得volatile非常适合用作状态标志位如上面的flag或者用于“一次性安全发布”的场景如单例模式的双重检查锁。但它不能保证复合操作的原子性比如volatile int i 0; i;这个i操作读取-修改-写入在多线程下依然是不安全的。2.2 原子性问题与“锁”的起源可见性解决了“一个线程写其他线程立刻能看到”的问题。但并发还有另一个恶魔原子性。所谓原子性就是指一个或多个操作要么全部执行成功要么全部不执行不会被打断。最经典的例子就是i。它看起来是一行代码但在JVM层面至少包含三个步骤1. 读取当前i的值2. 将值加13. 将新值写回i。如果两个线程同时执行i就可能出现“更新丢失”。public class AtomicityDemo { private static int count 0; private static final int THREAD_COUNT 100; private static final int PER_THREAD_ADD 10000; public static void main(String[] args) throws InterruptedException { Thread[] threads new Thread[THREAD_COUNT]; for (int i 0; i THREAD_COUNT; i) { threads[i] new Thread(() - { for (int j 0; j PER_THREAD_ADD; j) { count; // 非原子操作 } }); threads[i].start(); } for (Thread t : threads) { t.join(); } // 预期结果是 100 * 10000 1,000,000 System.out.println(理论值: (THREAD_COUNT * PER_THREAD_ADD) , 实际值: count); } }运行多次你会发现实际值几乎总是小于100万。这就是因为count不是原子操作。解决原子性问题最直观的想法就是“锁”。在Java中最基础的锁就是synchronized关键字。JUC中的ReentrantLock等锁工具其核心目标之一也是提供更灵活、更强大的原子性保障。但锁的引入又会带来性能开销和死锁风险这就需要我们深入理解锁的原理和JUC提供的各种锁优化方案。2.3 有序性与Happens-Before原则有序性指的是程序执行的顺序按照代码的先后顺序。但在并发环境下为了提高执行效率编译器和处理器可能会对指令进行重排序。这种重排序在单线程下不会影响最终结果遵循as-if-serial语义但在多线程下就可能引发问题。JMM通过Happens-Before规则来定义两个操作之间的偏序关系。如果操作A Happens-Before 操作B那么A的结果对B可见且A的执行顺序排在B之前。这是一组强大的规则包括程序次序规则一个线程内按照代码顺序书写在前面的操作先行发生于书写在后面的操作。管程锁定规则一个unlock操作先行发生于后面对同一个锁的lock操作。volatile变量规则对一个volatile变量的写操作先行发生于后面对这个变量的读操作。线程启动规则Thread对象的start()方法先行发生于此线程的每一个动作。线程终止规则线程中的所有操作都先行发生于对此线程的终止检测。线程中断规则对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。对象终结规则一个对象的初始化完成构造函数执行结束先行发生于它的finalize()方法的开始。传递性如果A Happens-Before B且B Happens-Before C那么A Happens-Before C。理解Happens-Before原则是理解volatile、synchronized、final等关键字并发语义以及正确分析复杂并发程序执行顺序的基础。3. JUC核心组件深度解析从AQS到各种锁JUC的精华很大一部分浓缩在它的锁框架和同步器上而这一切的基石就是AbstractQueuedSynchronizerAQS。3.1 AQSJUC锁体系的“心脏”你可以把AQS理解为一个构建锁和同步器的“脚手架”或“模板”。它内部维护了一个同步状态state和一个FIFO双向队列CLH队列的变体。这个队列用来存放所有等待获取锁的线程。AQS的核心思想共享资源用一个volatile int state来表示同步状态。比如在ReentrantLock中state0表示锁空闲state1表示锁被占用state1表示被同一个线程重入。队列管理如果线程获取资源失败比如尝试获取锁但state不为0AQS会将当前线程封装成一个Node节点通过CAS操作将其加入等待队列的尾部然后挂起线程通过LockSupport.park()。模板方法模式AQS定义了获取和释放资源的顶层骨架如acquire,release但把具体的资源获取和释放逻辑如“如何算获取成功”留给子类去实现。这主要通过重写tryAcquire、tryRelease等方法来完成。以ReentrantLock的非公平锁实现为例看lock()流程线程调用lock()。直接尝试CAS修改state从0到1。如果成功则设置当前线程为独占线程获取锁成功。这一步就是“非公平”的体现新来的线程直接插队尝试不管队列里有没有等待者。如果CAS失败调用AQS的acquire(1)方法。acquire会先调用子类实现的tryAcquire再次尝试获取非公平锁的实现里这里还会再尝试一次CAS插队。如果tryAcquire再次失败则将线程加入等待队列并挂起。注意事项理解AQS的关键在于理解其“自旋CAS队列”的协作机制。CASCompare-And-Swap是乐观锁的实现保证了状态修改的原子性。队列则用于管理竞争失败后的线程避免了忙等待busy-waiting造成的CPU空转。LockSupport.park/unpark是比Object.wait/notify更底层的线程阻塞/唤醒原语。3.2 ReentrantLock vs synchronized不只是性能之争synchronized是Java原生的关键字而ReentrantLock是JUC提供的类。选择哪一个这需要从多个维度对比特性synchronized(JDK 1.6优化后)ReentrantLock实现层面JVM层面由JVM实现锁的获取和释放。JDK层面通过Java代码AQS实现。锁的获取隐式获取和释放进入同步代码块自动获取退出自动释放。显式调用lock()和unlock()必须在finally块中释放锁。灵活性相对固定。非常灵活可尝试非阻塞获取(tryLock)、可中断获取(lockInterruptibly)、可超时获取(tryLock(timeout))。公平性非公平锁。可指定构造函数传入true创建公平锁false默认创建非公平锁。条件队列一个锁对应一个隐式的等待/通知机制(wait/notify)。一个锁可以绑定多个Condition对象实现更精细的线程等待/唤醒。性能JDK 1.6后引入偏向锁、轻量级锁、自旋锁等优化性能与ReentrantLock相差无几甚至在某些场景更优。在超高并发竞争下其可中断、超时等特性可能带来优势。核心选择建议优先使用synchronized语法简洁不易出错自动释放锁且经过JVM深度优化在大部分场景下性能足够好。这是《Effective Java》和很多专家的建议。需要高级功能时使用ReentrantLock当你确实需要可中断的锁获取避免死锁、尝试非阻塞获取锁、公平锁、或者需要多个条件谓词Condition时ReentrantLock是唯一选择。3.3 读写锁ReadWriteLock与StampedLock对于“读多写少”的场景使用独占锁如ReentrantLock会严重限制并发性因为读操作之间并不互斥。JUC提供了ReentrantReadWriteLock。核心思想允许多个线程同时读但只允许一个线程写且写写、读写互斥。读锁共享锁lock.readLock()。只要没有线程持有写锁多个线程可以同时获取读锁。写锁排他锁lock.writeLock()。一旦有线程获取了写锁其他任何线程无论是读还是写都无法再获取锁。潜在问题锁降级与写锁饥饿锁降级是指当前线程在持有写锁的情况下再获取读锁然后释放写锁的过程。ReentrantReadWriteLock支持锁降级但不支持锁升级先拿读锁再拿写锁因为后者容易造成死锁。写锁饥饿在读非常多、写很少的场景下写线程可能因为一直有读线程持有锁而长时间无法获取写锁。公平模式的ReentrantReadWriteLock可以缓解此问题但会牺牲部分吞吐量。更进一步的优化StampedLockStampedLock是JDK 1.8引入的它提供了一种乐观读的模式性能通常比ReentrantReadWriteLock更好。public class StampedLockDemo { private final StampedLock sl new StampedLock(); private double x, y; // 写方法使用写锁 void move(double deltaX, double deltaY) { long stamp sl.writeLock(); // 获取写锁返回一个戳记 try { x deltaX; y deltaY; } finally { sl.unlockWrite(stamp); // 释放写锁需要传入对应的戳记 } } // 乐观读方法 double distanceFromOrigin() { long stamp sl.tryOptimisticRead(); // 1. 尝试乐观读获取一个戳记 double currentX x, currentY y; // 2. 读取共享变量到局部变量 if (!sl.validate(stamp)) { // 3. 验证戳记检查在读过程中是否有写操作发生 stamp sl.readLock(); // 4. 如果验证失败升级为悲观读锁 try { currentX x; currentY y; } finally { sl.unlockRead(stamp); } } return Math.sqrt(currentX * currentX currentY * currentY); } }StampedLock的乐观读避免了真正的加锁操作只有在检测到数据被修改后才“升级”为悲观读锁重新读取。这在读远多于写且写冲突不频繁的场景下能极大提升性能。但它的API更复杂且不是可重入锁使用时要格外小心。实操心得StampedLock的性能虽好但复杂度高容易用错。除非经过压测证实ReentrantReadWriteLock确实是性能瓶颈否则建议优先使用后者。使用StampedLock时务必注意其tryOptimisticRead和validate的配合并且要理解其“戳记stamp”是用来协调锁状态的关键。4. 原子操作类与并发容器无锁编程的利器锁是保证线程安全的重要手段但锁的获取和释放本身就有开销还可能引起线程上下文切换和死锁。JUC提供了一系列基于CASCompare-And-Swap的无锁原子类以及线程安全的并发容器让我们能在很多场景下避免使用重量级锁。4.1 原子类Atomic家族与CAS原理java.util.concurrent.atomic包下提供了一系列原子类如AtomicInteger、AtomicLong、AtomicReference、AtomicStampedReference等。它们的核心方法是compareAndSetCAS。CAS操作包含三个参数内存位置V、预期原值A和新值B。当且仅当V的值等于A时处理器才会用B更新V的值否则不执行更新。整个操作是一个原子指令现代CPU普遍支持。public class AtomicDemo { private static AtomicInteger atomicCount new AtomicInteger(0); private static final int THREAD_COUNT 100; private static final int PER_THREAD_ADD 10000; public static void main(String[] args) throws InterruptedException { Thread[] threads new Thread[THREAD_COUNT]; for (int i 0; i THREAD_COUNT; i) { threads[i] new Thread(() - { for (int j 0; j PER_THREAD_ADD; j) { atomicCount.incrementAndGet(); // 原子自增 } }); threads[i].start(); } for (Thread t : threads) { t.join(); } // 结果总是正确的 1,000,000 System.out.println(AtomicInteger 结果: atomicCount.get()); } }incrementAndGet()内部就是通过循环CAS实现的直到成功为止。这避免了锁的使用性能很高。CAS的经典问题ABA问题线程1读取变量X的值为A然后线程2将X改为B接着又改回A。此时线程1执行CAS发现X的值还是A于是操作成功。但实际上这个A已经不是最初的那个A了中间状态发生了变化。对于引用类型或涉及版本控制的场景这可能有问题。解决方案AtomicStampedReference。它通过一个int类型的“戳记stamp”来记录版本号CAS时同时比较值和版本号。注意事项CAS适合竞争不激烈的场景。在超高并发下如果多个线程反复CAS失败自旋会造成CPU空转消耗大量资源。此时LongAdderJDK 1.8引入可能是更好的选择。LongAdder采用“分段”思想内部维护一个Cell数组最终结果由base和所有cell的值累加得到在高并发写场景下能有效分散竞争性能远优于AtomicLong。4.2 并发容器告别Collections.synchronizedXXX早期我们使用Collections.synchronizedList(new ArrayList())来获得一个线程安全的List但它是在所有方法上加synchronized性能是瓶颈。JUC提供了一套高性能的并发容器。ConcurrentHashMap这是最重要的并发容器。在JDK 1.7中它采用分段锁Segment机制将数据分成一段段存储每段配一把锁这样不同段的操作可以并发。在JDK 1.8中它做了巨大改进摒弃了分段锁改用Node synchronized CAS的实现。put流程简化计算key的hash定位到数组下标。如果桶为空用CAS插入新节点。如果桶不为空则用synchronized锁住桶的头节点再进行链表或红黑树的插入操作。优势锁的粒度更细锁住单个桶或树节点并发度更高扩容时支持多线程协助迁移数据。CopyOnWriteArrayList/CopyOnWriteArraySet写时复制容器。每次修改add, set, remove时并不直接在原数组上操作而是先复制一份新的底层数组在新数组上完成修改最后将容器的引用指向新数组。优点读操作完全无锁性能极高且读到的数据永远是一致的因为读操作面对的是不可变的快照。缺点写操作开销大复制整个数组且存在“弱一致性”问题——写操作完成后读线程可能无法立即看到最新结果。适合读多写极少的场景如监听器列表、缓存黑名单等。阻塞队列BlockingQueue如ArrayBlockingQueue有界数组队列、LinkedBlockingQueue可选有界链表队列、PriorityBlockingQueue支持优先级、SynchronousQueue不存储元素的直接交接队列。它们是实现生产者-消费者模式的利器提供了put/take阻塞和offer/poll超时或立即返回等API。选择指南需要高并发键值存储首选ConcurrentHashMap。需要线程安全的列表/集合且读远大于写考虑CopyOnWriteArrayList/Set。需要实现生产者-消费者模式根据容量、排序等需求选择对应的BlockingQueue。5. 线程池ThreadPoolExecutor深度剖析与最佳实践直接创建和管理线程存在诸多问题线程创建/销毁开销大、资源不受控可能创建过多线程耗尽资源、缺乏统一管理。线程池是解决这些问题的标准答案而ThreadPoolExecutor是其最核心的实现。5.1 七大核心参数与工作原理创建线程池最推荐的方式是直接使用ThreadPoolExecutor的构造函数而不是Executors的工厂方法后者可能隐藏风险如FixedThreadPool和SingleThreadExecutor使用无界队列可能导致OOM。public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueueRunnable workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)corePoolSize核心线程数线程池的基本大小。即使线程空闲也会保留在池中除非设置了allowCoreThreadTimeOut。maximumPoolSize最大线程数线程池允许创建的最大线程数。keepAliveTime线程空闲时间当线程数超过核心线程数时多余的空闲线程在等待新任务时的最长存活时间。unit时间单位keepAliveTime的时间单位。workQueue工作队列用于存放待执行任务的阻塞队列。常见的有LinkedBlockingQueue无界队列如果初始化未指定容量可能导致OOM。ArrayBlockingQueue有界队列。SynchronousQueue不存储元素每个插入操作必须等待另一个线程的移除操作。PriorityBlockingQueue带优先级的无界队列。threadFactory线程工厂用于创建新线程。可以自定义线程名、优先级、是否为守护线程等便于监控和排查问题。handler拒绝策略当线程池和队列都满了无法处理新任务时采取的拒绝策略。内置策略有AbortPolicy默认直接抛出RejectedExecutionException。CallerRunsPolicy由调用者线程提交任务的线程自己执行该任务。DiscardPolicy直接丢弃任务不做任何处理。DiscardOldestPolicy丢弃队列中最老的一个任务然后尝试提交当前任务。线程池工作流程务必牢记提交一个任务。如果当前运行的线程数 corePoolSize则创建新线程来执行任务即使有其他空闲的核心线程。如果运行的线程数 corePoolSize则将任务放入workQueue。如果队列已满且运行的线程数 maximumPoolSize则创建新的非核心线程来执行任务。如果队列已满且运行的线程数已达到maximumPoolSize则根据handler执行拒绝策略。核心避坑点corePoolSize、maximumPoolSize和workQueue的类型共同决定了线程池的行为。例如使用无界队列如LinkedBlockingQueue未设容量那么maximumPoolSize参数将失效因为队列永远不会满永远不会创建超过核心线程数的线程。同时无界队列可能堆积大量任务导致OOM。5.2 线程池的监控、调优与优雅关闭如何设置合理的线程池参数这是一个没有银弹的问题需要根据任务类型CPU密集型、IO密集型、系统资源、业务目标来权衡。CPU密集型任务线程数不宜过多一般设置为CPU核心数 1。过多会导致频繁的线程上下文切换降低性能。IO密集型任务由于线程大部分时间在等待IO如网络、数据库可以设置更多线程以便在等待时让CPU去执行其他线程的任务。经验公式CPU核心数 * (1 平均等待时间 / 平均计算时间)。例如如果计算时间与等待时间各占一半可设为2NN为CPU核心数。在实际中通常需要压测来确定。队列选择需要快速响应、避免任务堆积使用SynchronousQueue或容量较小的ArrayBlockingQueue允许一定程度的任务缓冲使用LinkedBlockingQueue务必设置合理容量。如何监控线程池可以通过ThreadPoolExecutor提供的方法获取运行状态getPoolSize()当前线程池中的线程数。getActiveCount()正在执行任务的线程数。getCompletedTaskCount()已完成的任务总数。getQueue().size()队列中等待的任务数。 在生产环境中通常需要将这些指标接入监控系统如Prometheus Grafana。如何优雅关闭线程池调用shutdown()或shutdownNow()。shutdown()平缓关闭。不再接受新任务但会执行完已提交的任务包括队列中的。shutdownNow()立即关闭。尝试中断所有正在执行的任务并返回队列中未执行的任务列表。 通常我们会先调用shutdown()然后配合awaitTermination(long timeout, TimeUnit unit)等待一段时间。如果超时后仍未关闭再调用shutdownNow()进行强制关闭。executor.shutdown(); // 启动有序关闭 try { if (!executor.awaitTermination(60, TimeUnit.SECONDS)) { // 等待60秒 executor.shutdownNow(); // 强制关闭 if (!executor.awaitTermination(60, TimeUnit.SECONDS)) { System.err.println(线程池未能完全关闭); } } } catch (InterruptedException ie) { executor.shutdownNow(); Thread.currentThread().interrupt(); // 保留中断状态 }6. 并发工具类与高级同步模式除了锁和容器JUC还提供了一些“开箱即用”的高级同步工具能极大简化复杂并发逻辑的编写。6.1 CountDownLatch、CyclicBarrier、Semaphore这三者都是基于AQS实现的同步辅助类但用途各异。CountDownLatch倒计时闩锁允许一个或多个线程等待其他线程完成操作。构造时传入一个计数N。线程调用await()等待其他线程完成任务后调用countDown()将计数减1。当计数减为0时所有等待的线程被唤醒继续执行。一次性使用计数无法重置。典型场景主线程等待多个子线程初始化完成模拟并发测试同时发起请求。CyclicBarrier循环屏障让一组线程互相等待直到所有线程都到达一个公共屏障点然后一起继续执行。构造时传入参与线程数N和一个可选的Runnable屏障动作。线程调用await()表示自己已到达屏障并阻塞。当第N个线程到达后所有线程被唤醒屏障动作如果有执行然后屏障重置可以重复使用。典型场景多阶段任务需要所有线程完成当前阶段才能进入下一阶段如数据分片计算最后合并结果。Semaphore信号量用来控制同时访问特定资源的线程数量。构造时传入许可数permits。线程通过acquire()获取一个许可如果无许可则阻塞访问完资源后通过release()释放许可。典型场景数据库连接池限流、限流访问API。6.2 CompletableFuture异步编程的“瑞士军刀”Future接口可以获取异步任务的结果但它的获取方式是阻塞的get()且不方便组合多个异步任务。CompletableFuture是JDK 1.8引入的它实现了Future和CompletionStage接口提供了强大的异步编程能力。核心优势显式完成你可以手动设置CompletableFuture的结果complete(value)或completeExceptionally(throwable)。非阻塞结果获取可以通过回调thenApply,thenAccept,thenRun,whenComplete等来处理结果无需阻塞。任务组合与链式调用可以将多个异步任务以流水线thenCompose、聚合thenCombine、并行allOf,anyOf等方式组合起来。// 示例模拟从远程服务获取用户信息、订单信息然后合并处理 public CompletableFutureString getUserInfoAsync(String userId) { return CompletableFuture.supplyAsync(() - { // 模拟网络调用 try { Thread.sleep(100); } catch (InterruptedException e) { } return UserInfo of userId; }); } public CompletableFutureString getOrderInfoAsync(String orderId) { return CompletableFuture.supplyAsync(() - { try { Thread.sleep(150); } catch (InterruptedException e) { } return OrderInfo of orderId; }); } public void processUserAndOrder(String userId, String orderId) { CompletableFutureString userFuture getUserInfoAsync(userId); CompletableFutureString orderFuture getOrderInfoAsync(orderId); userFuture.thenCombine(orderFuture, (userInfo, orderInfo) - { // 当两个Future都完成时合并它们的结果 return Combined Result: userInfo | orderInfo; }).thenAccept(combinedResult - { // 异步处理合并后的结果 System.out.println(combinedResult); }).exceptionally(ex - { // 异常处理 System.err.println(Error: ex.getMessage()); return null; }); // 主线程可以继续做其他事情不会被阻塞 System.out.println(Main thread continues...); }CompletableFuture默认使用ForkJoinPool.commonPool()作为执行器你也可以通过supplyAsync(Supplier, Executor)指定自定义的线程池这对于IO密集型任务避免占用公共池非常重要。实操心得CompletableFuture的API非常丰富学习曲线较陡。建议从supplyAsync/runAsync启动异步任务、thenApply转换结果、thenAccept消费结果、exceptionally异常处理这几个最常用的方法开始。理解“回调地狱”与链式调用的区别合理使用thenCompose扁平化和thenCombine合并来组合任务。对于多个任务的聚合allOf和anyOf非常有用。7. 常见问题排查与性能调优实战理论学习最终要落地到解决问题。下面记录几个在实战中高频出现的问题和排查思路。7.1 死锁的诊断与预防死锁的四个必要条件互斥、持有并等待、不可剥夺、循环等待。一旦发生相关线程会永远阻塞。诊断工具jstack最常用的命令行工具。jstack pid可以打印出Java进程的所有线程栈信息。在输出中搜索deadlock关键词或者仔细查看线程状态为BLOCKED的线程分析它们等待的锁和被谁持有往往能发现循环等待链。JConsole / VisualVM图形化工具连接到Java进程后在“线程”选项卡可以检测到死锁并直观地显示哪些线程在等待哪些锁。预防策略避免嵌套锁尽量只获取一个锁。如果必须获取多个确保所有线程以相同的顺序获取锁全局固定的锁顺序。使用定时锁ReentrantLock的tryLock(long timeout, TimeUnit unit)方法可以指定超时时间获取锁失败后可以释放已持有的锁进行回退或重试。使用更高级的并发工具有时可以用ConcurrentHashMap、CopyOnWriteArrayList等无锁或细粒度锁的容器来替代需要同步的数据结构。7.2 线程池的常见“坑”任务堆积导致OOM使用了无界队列如Executors.newFixedThreadPool内部用的LinkedBlockingQueue且任务生产速度持续大于消费速度。解决方案使用有界队列并设置合理的拒绝策略如CallerRunsPolicy让调用者线程执行起到负反馈作用。线程泄漏任务中抛出了未捕获的异常导致执行该任务的线程提前终结线程池会创建新的线程补充。如果任务总是异常可能导致线程频繁创建销毁。解决方案在任务代码最外层进行try-catch或者实现自定义的ThreadFactory为线程设置UncaughtExceptionHandler。上下文切换开销大线程池核心/最大线程数设置过大尤其是CPU密集型任务导致大量时间花在线程切换上CPU使用率很高但吞吐量上不去。解决方案根据任务类型调整线程数使用监控工具观察线程状态和CPU使用情况。ThreadLocal内存泄漏线程池中的线程是复用的如果任务中使用了ThreadLocal且没有及时调用remove()清理那么该线程执行下一个任务时可能还保留着上一个任务的数据造成混乱或内存泄漏因为ThreadLocal的Entry是弱引用但value是强引用线程不结束value就不会被回收。解决方案务必在try-finally块中或在任务结束时调用ThreadLocal.remove()。7.3 高并发下的性能优化思路缩小锁粒度这是最有效的优化之一。将一把大锁拆分成多把小锁。ConcurrentHashMap从分段锁到锁桶的演进就是经典案例。评估你的数据结构看是否可以用更细粒度的锁来保护。无锁化设计优先考虑使用原子类AtomicInteger、LongAdder、CopyOnWriteArrayList等无锁或写时复制容器。对于状态简单的计数器LongAdder在高并发下比AtomicLong性能好得多。减少锁持有时间只在必须同步的代码块上加锁。能放在锁外部的计算、IO操作尽量放在外部。例如从共享Map中获取数据后对数据的处理如果不涉及共享状态就应立即释放锁。使用读写锁分离对于明显的读多写少场景用ReentrantReadWriteLock或StampedLock替代独占锁。异步与非阻塞使用CompletableFuture、反应式编程如Reactor, RxJava将阻塞调用异步化用回调代替线程等待可以极大提升系统的吞吐量和资源利用率。合理使用线程池根据业务负载动态调整线程池参数一些框架支持动态调整。对于不同的任务类型CPU密集、IO密集、混合型使用不同的、隔离的线程池避免相互影响。并发编程的学习是一个持续的过程从理解基本概念到熟练使用工具再到能诊断和解决线上复杂的并发问题每一步都需要大量的实践和思考。这份笔记试图为你搭建一个从底层原理到上层应用的完整知识框架但真正的掌握还需要你在自己的项目中不断尝试、踩坑和总结。记住在并发世界里“先保证正确性再考虑优化”是一条黄金法则。