多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

彻底搞懂JUC:Java并发编程2万字详解

彻底搞懂JUC:Java并发编程2万字详解 一、什么是JUCJUC 是java.util.concurrent包及其子包的简称它是 JDK 5 引入的一套专门用于并发编程的工具库。在 JUC 出现之前Java 开发者主要依靠synchronized、wait、notify、volatile这些底层原语来应对多线程问题。这些原语虽然能解决问题但存在三个明显痛点一是无法实现灵活的锁控制比如锁超时、可中断、公平锁二是缺少高效、线程安全的并发容器和原子操作类三是线程池、同步工具等高级能力需要重复造轮子。JUC 的出现彻底改变了这一局面。它把工业界最常用、最稳定的一整套并发组件沉淀了下来主要由以下几大板块构成原子类Atomic基于 CAS 实现的无锁原子操作如AtomicInteger、AtomicReference。锁体系Locks以AbstractQueuedSynchronizerAQS为核心的显示锁如ReentrantLock、ReentrantReadWriteLock、StampedLock。并发集合Concurrent Collections如ConcurrentHashMap、CopyOnWriteArrayList、各类BlockingQueue。线程池Executor以ThreadPoolExecutor为核心的线程管理框架。同步工具类Tools如CountDownLatch、CyclicBarrier、Semaphore、Exchanger。异步编程CompletableFuture用于编排异步任务和响应式风格编程。学习 JUC 的目标不仅是“会用”更是要理解它背后的底层机制volatile 的内存语义、CAS 的原子性保障、AQS 的同步队列模型、线程池的调度原理。只有理解了这些底层机制才能在实际项目中做出正确的并发选型写出真正线程安全且高效的代码。二、并发编程的三大基石在深入 JUC 之前必须先理清并发的三大基础问题可见性、原子性、有序性。几乎所有 JUC 组件本质上都是在解决这三个问题中的一个或多个。2.1 可见性一个线程改了另一个线程能看到吗在 JMMJava 内存模型中每个线程都有自己的工作内存所有变量都存储在主内存中。线程对变量的读写都要先操作自己的工作内存再与主内存交互。这种设计带来一个问题线程 A 修改了一个普通变量线程 B 可能根本看不到这个修改因为 B 读到的是自己工作内存中的旧值。javapublic class VisibilityDemo { private static boolean flag false; public static void main(String[] args) throws InterruptedException { new Thread(() - { while (!flag) { // 如果没有可见性保障这里可能永远循环 } System.out.println(线程结束); }).start(); Thread.sleep(1000); flag true; // 主线程修改 flag } }上面的代码理论上主线程把flag改为true后子线程应该退出循环。但由于缺乏可见性保障子线程可能会一直循环下去。volatile关键字正是为解决可见性问题而生被volatile修饰的变量写操作会立即刷新回主内存读操作会强制从主内存读取从而保证所有线程看到的值一致。2.2 原子性一次操作要么全做要么全不做原子性指的是一个操作或多个操作在执行过程中不会被其他线程打断。最简单的例子就是i它并不是一个原子操作而是由“读取、加一、写回”三步组成。如果两个线程同时执行i可能两个线程都读到了同一个旧值最终只加了一次导致结果丢失。javapublic class AtomicityDemo { private static int count 0; public static void main(String[] args) throws InterruptedException { Thread t1 new Thread(() - { for (int i 0; i 10000; i) count; }); Thread t2 new Thread(() - { for (int i 0; i 10000; i) count; }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(最终结果 count); // 通常小于 20000 } }解决原子性问题有两种经典手段一种是加锁比如synchronized或ReentrantLock另一种就是 JUC 中的 CAS 原子操作如AtomicInteger.incrementAndGet()。2.3 有序性指令重排序带来的诡异问题为了提升执行效率编译器和 CPU 会在不影响单线程语义的前提下对指令进行重排序。但在多线程环境下重排序可能导致另一个线程看到不符合“代码书写顺序”的执行结果最典型的就是双重检查锁的instance new Singleton()问题。这句代码实际包含三个步骤分配内存、初始化对象、将引用指向内存地址而后两步可能被重排序导致其他线程拿到一个尚未初始化完成的对象。volatile除了保证可见性还能通过内存屏障禁止指令重排序因此双重检查锁中的单例引用必须声明为volatile。理解这三大基石是把 JUC 学透的前提。三、深入volatile与JMM内存模型volatile是 JUC 中最轻量级的同步机制也是理解 JMM 的绝佳入口。它只保证可见性和有序性不保证原子性这一点必须牢记。3.1 volatile的两层语义可见性当一个线程修改了 volatile 变量的值新值对于其他线程来说是立即可见的。这是通过“写后立即刷回主内存、读前强制从主内存加载”实现的。禁止指令重排序通过插入内存屏障Memory Barrier来禁止特定位置的重排序保证特定代码的执行顺序。在 JVM 层面volatile 的写操作前会插入StoreStore屏障、写操作后插入StoreLoad屏障读操作后插入LoadLoad和LoadStore屏障。这些屏障确保了 volatile 变量与其他普通变量的读写不会发生越界重排序。3.2 volatile不保证原子性javapublic class VolatileAtomicDemo { private static volatile int count 0; public static void main(String[] args) throws InterruptedException { for (int i 0; i 10; i) { new Thread(() - { for (int j 0; j 1000; j) { count; // 读、加、写三步不具备原子性 } }).start(); } Thread.sleep(2000); System.out.println(count); // 结果大概率不是 10000 } }因为count是“读-改-写”复合操作volatile 只能保证每次读到的是最新值但无法阻止两个线程同时读到同一个值后再各自加一。要保证原子性要么改为AtomicInteger要么加锁。3.3 happens-before规则JMM 定义了一系列happens-before规则用来约束两个操作之间的可见性和顺序关系。volatile 涉及的规则包括程序次序规则同一个线程内前面的操作 happens-before 后面的操作。volatile 变量规则对一个 volatile 变量的写 happens-before 后续对该变量的读。传递性如果 A happens-before BB happens-before C那么 A happens-before C。正是这些规则使得 volatile 写之前的普通变量修改对 volatile 读之后的代码也是可见的。理解 happens-before 是理解所有 JUC 同步组件内存语义的钥匙。四、CAS与Unsafe无锁并发的根基CASCompare And Swap比较并交换是 JUC 中所有原子类和 AQS 的核心底层机制。它可以不通过加锁就实现“读-改-写”的原子性从而获得远高于悲观锁的性能。4.1 CAS的核心思想CAS 包含三个操作数内存地址 V、期望值 A、新值 B。执行时先比较 V 处的当前值是否等于 A如果相等就把 V 更新为 B如果不相等说明有其他线程已经修改过本次操作失败通常会重新读取最新值再次尝试自旋。整个过程由 CPU 的原子指令保证原子性例如 x86 架构下的cmpxchg指令。javapublic class CasDemo { private static final AtomicInteger count new AtomicInteger(0); public static void main(String[] args) throws InterruptedException { for (int i 0; i 10; i) { new Thread(() - { for (int j 0; j 1000; j) { // 等价于 count但底层是 CAS 循环保证原子性 count.incrementAndGet(); } }).start(); } Thread.sleep(2000); System.out.println(count.get()); // 恒为 10000 } }4.2 CAS的三大问题ABA 问题一个值从 A 变成 B 又变回 ACAS 检测时无法发现它曾经变化过。虽然多数场景下不影响正确性但在链表、栈等结构中有可能引发问题。解决方式是带版本号如AtomicStampedReference或AtomicMarkableReference。循环时间长开销大高并发争用时CAS 可能长时间自旋失败白白消耗 CPU。这也是为什么 JDK 8 加入了LongAdder通过分段累加降低竞争。只能保证一个共享变量的原子操作CAS 一次只能操作一个变量无法直接原子地更新多个变量。解决方式是使用AtomicReference封装复合对象或使用锁。4.3 Unsafe类CAS 的底层能力来自sun.misc.Unsafe类。它提供了一系列compareAndSwapInt、compareAndSwapObject、compareAndSwapLong等 native 方法可以直接操作内存地址。Unsafe 还提供了park和unpark方法是 AQS 阻塞和唤醒线程的底层支撑。对于普通应用开发者来说不推荐直接使用 Unsafe但理解它是掌握 JUC 底层原理的重要一环。javapublic class CasLockDemo { private final AtomicInteger state new AtomicInteger(0); public void lock() { // 自旋获取锁0 代表未加锁1 代表已加锁 while (!state.compareAndSet(0, 1)) { // 获取失败就自旋等待 } } public void unlock() { state.set(0); } }五、原子类从AtomicInteger到LongAdderJUC 的java.util.concurrent.atomic包提供了一整套基于 CAS 的无锁原子操作类是解决单变量原子性问题最优雅的方案。5.1 基本类型原子类AtomicInteger原子 int常用incrementAndGet、getAndIncrement、compareAndSet。AtomicLong原子 long与 AtomicInteger 类似。AtomicBoolean原子 boolean常用于标志位控制。javapublic class AtomicIntegerDemo { public static void main(String[] args) { AtomicInteger ai new AtomicInteger(10); System.out.println(ai.incrementAndGet()); // 先加再获取11 System.out.println(ai.getAndIncrement()); // 先获取再加11此时值为 12 System.out.println(ai.get()); // 12 System.out.println(ai.compareAndSet(12, 100)); // true System.out.println(ai.get()); // 100 } }5.2 数组类型原子类AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray通过索引对数组元素进行原子操作保证数组中每一个元素的更新都是原子的。需要注意的是数组类型原子类保证的是单个元素的原子性而不是整个数组的原子性。5.3 引用类型原子类AtomicReference对任意对象引用进行原子更新。AtomicStampedReference带版本号时间戳的引用用于解决 ABA 问题。AtomicMarkableReference带布尔标记的引用适合“是否已被修改”这类简单标记场景。javapublic class AtomicReferenceDemo { static class User { String name; int age; User(String name, int age) { this.name name; this.age age; } } public static void main(String[] args) { AtomicReferenceUser ref new AtomicReference(new User(张三, 20)); User newUser new User(李四, 30); ref.compareAndSet(ref.get(), newUser); System.out.println(ref.get().name); // 李四 } }5.4 字段更新器与LongAdderAtomicIntegerFieldUpdater等字段更新器可以对普通类中的 volatile 字段进行原子更新但要求字段必须是volatile类型且不能是 private。实际项目中更常用的是LongAdder和DoubleAdder。JDK 8 引入的LongAdder将内部值拆分为一个 base 和多个 Cell不同的线程大概率更新不同的 Cell从而降低 CAS 的竞争程度。它在超高并发计数场景下性能远胜AtomicLong但sum()方法并不是强一致的精确值适合统计而非严格的逻辑判断。javapublic class LongAdderDemo { public static void main(String[] args) throws InterruptedException { LongAdder adder new LongAdder(); for (int i 0; i 10; i) { new Thread(() - { for (int j 0; j 10000; j) { adder.increment(); } }).start(); } Thread.sleep(2000); System.out.println(adder.sum()); // 100000 } }六、AQS锁体系的灵魂AbstractQueuedSynchronizer抽象队列同步器是整个 JUC 锁体系的基石。ReentrantLock、ReentrantReadWriteLock、Semaphore、CountDownLatch等同步组件本质上都是基于 AQS 的模板方法模式实现的。6.1 AQS的核心设计AQS 内部维护了一个int 类型的 state 变量和一个FIFO 双向队列。state 用来表示同步状态不同组件对它赋予了不同含义在ReentrantLock中 state 表示锁的重入次数在Semaphore中表示许可数量在CountDownLatch中表示剩余计数。当线程尝试获取同步状态失败时会被包装成Node节点加入队列尾部并进入阻塞状态当同步状态被释放时AQS 会唤醒队列头部等待的节点让其再次尝试获取同步状态。整个加锁、排队、唤醒流程都围绕 state 的 CAS 更新展开。6.2 AQS的模板方法模式AQS 采用模板方法模式把“排队、阻塞、唤醒”这些复杂的通用逻辑封装在框架方法中把“是否允许获取、如何设置状态”这些差异逻辑留给子类实现。使用 AQS 时子类通常需要重写以下方法tryAcquire(int arg)独占模式尝试获取同步状态成功返回 true失败返回 false。tryRelease(int arg)独占模式尝试释放同步状态。tryAcquireShared(int arg)/tryReleaseShared(int arg)共享模式尝试获取/释放同步状态。isHeldExclusively()判断当前同步器是否被当前线程独占。而acquire()、release()、acquireShared()、releaseShared()这些流程方法由 AQS 提供开发者直接调用即可。6.3 自定义AQS同步器javapublic class SimpleLock { private final Sync sync new Sync(); private static class Sync extends AbstractQueuedSynchronizer { Override protected boolean tryAcquire(int arg) { // 0 表示未加锁1 表示已加锁 return compareAndSetState(0, 1); } Override protected boolean tryRelease(int arg) { setState(0); return true; } Override protected boolean isHeldExclusively() { return getState() 1; } } public void lock() { sync.acquire(1); } public void unlock() { sync.release(1); } }这段代码展示了 AQS 的魅力我们只重写了三个方法就得到了一个具备排队、阻塞、唤醒能力的锁框架。实际生产项目中一般不重复造轮子直接使用ReentrantLock等成熟实现即可。七、ReentrantLock详解ReentrantLock 是最常用的显示锁基于 AQS 实现具备可重入、可中断、可超时、公平/非公平可选等特性是对 synchronized 的重要补充。7.1 ReentrantLock与synchronized对比对比项synchronizedReentrantLock实现方式JVM 层面的关键字依赖 Monitor 对象JDK 层面的类基于 AQS 实现锁释放自动释放作用于代码块或方法必须手动 unlock通常写在 finally 中公平性非公平可选公平或非公平可中断不可中断lockInterruptibly 可响应中断超时获取不支持tryLock(timeout, unit) 支持条件队列单一 wait/notify多个 Condition可精确唤醒7.2 公平锁与非公平锁非公平锁是默认实现新来的线程可以直接参与抢锁无需先进入队列因此吞吐量更高但可能造成某些线程长时间等待。公平锁则严格按照 FIFO 队列分配锁减少饥饿但频繁的线程切换会降低吞吐量。在没有特别公正性要求的场景下推荐使用非公平锁。javaReentrantLock fairLock new ReentrantLock(true); // 公平锁 ReentrantLock nonFairLock new ReentrantLock(); // 非公平锁默认7.3 可重入机制可重入锁允许同一个线程多次获取同一把锁而不会死锁。AQS 中通过 state 记录重入次数同一个线程每获取一次锁state 加 1每释放一次锁state 减 1当 state 归零时锁才真正释放。javaReentrantLock lock new ReentrantLock(); public void outer() { lock.lock(); try { System.out.println(外层方法); inner(); // 同一线程再次获取同一把锁 } finally { lock.unlock(); } } private void inner() { lock.lock(); try { System.out.println(内层方法); } finally { lock.unlock(); } }7.4 Condition条件队列Condition 提供比 wait/notify 更精细的线程协作能力多个 Condition 可以对应多个等待队列从而实现精确唤醒。javaReentrantLock lock new ReentrantLock(); Condition notEmpty lock.newCondition(); Condition notFull lock.newCondition(); public void produce() throws InterruptedException { lock.lock(); try { while (queue.isFull()) { notFull.await(); // 队列满等待消费 } queue.add(data); notEmpty.signal(); // 唤醒消费者 } finally { lock.unlock(); } }八、ReentrantReadWriteLock与StampedLock在很多业务场景中读操作远多于写操作。如果所有线程都使用互斥锁会严重浪费并发能力。ReentrantReadWriteLock 通过读写分离提升读并发多个线程可以同时持有读锁只有写锁才是独占的。8.1 读写锁特性读读共享多个读线程可以同时访问。读写互斥有写锁时读锁被阻塞反之亦然。写写互斥同一时刻只能有一个写线程。写锁可降级为读锁读锁不能升级为写锁。javaprivate final ReentrantReadWriteLock rwLock new ReentrantReadWriteLock(); private final Lock readLock rwLock.readLock(); private final Lock writeLock rwLock.writeLock(); private final MapString, String cache new HashMap(); public String get(String key) { readLock.lock(); try { return cache.get(key); } finally { readLock.unlock(); } } public void put(String key, String value) { writeLock.lock(); try { cache.put(key, value); } finally { writeLock.unlock(); } }8.2 锁降级锁降级指在持有写锁的情况下先获取读锁再释放写锁从而保证数据可见性并降低锁竞争。常见于写完数据后需要马上读取结果的场景。javapublic void updateAndRead(String key, String value) { writeLock.lock(); try { cache.put(key, value); readLock.lock(); // 先加读锁 } finally { writeLock.unlock(); // 再释放写锁 } try { // 此时仍持有读锁可安全读取最新数据 System.out.println(cache.get(key)); } finally { readLock.unlock(); } }8.3 StampedLock乐观读的进阶方案ReadWriteLock 的读锁仍然会阻塞写锁JDK 8 引入的 StampedLock 提供了乐观读模式读线程先获取一个戳记直接读取数据读取后再校验戳记是否发生变化。如果期间没有写操作读过程无需加锁如果发生写操作则升级为悲观读重试。适合读多写极少的场景。九、并发集合线程安全的容器传统线程安全的集合如 Vector、Hashtable 以及Collections.synchronizedList包装器都是通过对整个容器加锁来保证安全并发性能极差。JUC 提供了一套细粒度锁或无锁实现的并发集合是高性能并发编程的重要基础。9.1 ConcurrentHashMapConcurrentHashMap 是并发场景下最常用的 Map 实现。JDK 7 采用分段锁Segment降低锁粒度JDK 8 后放弃了分段锁改为对数组桶节点加 synchronized 锁并结合 CAS 对空桶进行无锁插入。读操作大部分情况下不加锁依靠 volatile 保证可见性因此读性能非常高。javaConcurrentHashMapString, Integer map new ConcurrentHashMap(); map.put(key, 1); // computeIfAbsent 保证首次初始化原子完成 Integer v map.computeIfAbsent(key, k - 1); System.out.println(v); // 1需要注意ConcurrentHashMap 不允许 null 键和 null 值它的size()方法是弱一致的多线程同时修改时结果不保证精确。9.2 CopyOnWriteArrayListCopyOnWriteArrayList 采用“写时复制”策略每次写操作都会复制一份新数组在新数组上修改后再替换旧引用读操作直接读取当前数组完全不加锁。它适合读多写极少的场景例如配置列表、监听器列表。写操作频繁时会有大量拷贝开销不适合使用。javaCopyOnWriteArrayListString list new CopyOnWriteArrayList(); list.add(A); list.add(B); // 增强 for 遍历时可以直接删除不会抛 ConcurrentModificationException for (String s : list) { if (A.equals(s)) { list.remove(s); } } System.out.println(list); // [B]9.3 BlockingQueue阻塞队列阻塞队列是生产者-消费者模型的核心组件它提供 put、take 等阻塞方法队列满时 put 阻塞队列空时 take 阻塞。常用实现包括 ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、DelayQueue、PriorityBlockingQueue。javaBlockingQueueString queue new LinkedBlockingQueue(10); // 生产者 new Thread(() - { try { queue.put(消息); // 队列满时阻塞 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }).start(); // 消费者 new Thread(() - { try { String msg queue.take(); // 队列空时阻塞 System.out.println(msg); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }).start();十、线程池ThreadPoolExecutor深入解析线程的创建和销毁成本很高频繁创建线程还会导致线程数量失控。线程池通过池化技术对线程进行统一管理既可以复用线程控制并发规模又能实现任务排队和拒绝策略。10.1 七大核心参数javapublic ThreadPoolExecutor( int corePoolSize, // 核心线程数 int maximumPoolSize, // 最大线程数 long keepAliveTime, // 非核心线程空闲存活时间 TimeUnit unit, // 时间单位 BlockingQueueRunnable workQueue, // 任务队列 ThreadFactory threadFactory, // 线程工厂 RejectedExecutionHandler handler // 拒绝策略 ) { ... }10.2 执行流程当任务提交后如果当前线程数小于 corePoolSize直接创建新线程执行任务。如果线程数已达到 corePoolSize新任务进入 workQueue 排队等待。如果 workQueue 已满且线程数小于 maximumPoolSize创建非核心线程执行任务。如果线程数达到 maximumPoolSize 且队列已满触发拒绝策略。非核心线程空闲超过 keepAliveTime 后会被回收核心线程默认常驻。10.3 四种拒绝策略AbortPolicy默认抛出 RejectedExecutionException。CallerRunsPolicy由提交任务的调用线程执行该任务。DiscardPolicy直接丢弃新任务不抛异常。DiscardOldestPolicy丢弃队列中最老的任务再尝试提交新任务。javaThreadPoolExecutor executor new ThreadPoolExecutor( 4, // 核心线程数 8, // 最大线程数 60, // 空闲存活时间 TimeUnit.SECONDS, new ArrayBlockingQueue(100), // 有界队列 runnable - { Thread t new Thread(runnable); t.setName(biz-pool- t.getId()); return t; }, new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略 );10.4 为什么不建议使用Executors快捷方法Executors.newFixedThreadPool等快捷方法内部使用 LinkedBlockingQueue 无界队列极端情况下可能堆积大量任务导致 OOMnewCachedThreadPool允许创建数量不设上限的线程也可能导致资源耗尽。阿里巴巴 Java 开发规范明确要求线程池必须通过 ThreadPoolExecutor 手动创建以明晰资源边界和风险。十一、同步工具类并发协作的利器JUC 提供了一系列同步辅助类帮助多个线程以更优雅的方式完成协作避免手写复杂的 wait/notify 逻辑。11.1 CountDownLatch等待一组任务完成CountDownLatch 通过一个计数器实现“一等多”或“多等一”的同步。主线程调用 await() 等待其他线程完成任务后调用 countDown() 递减计数计数归零后主线程继续执行。计数器只能使用一次不能重置。javaint taskCount 3; CountDownLatch latch new CountDownLatch(taskCount); for (int i 0; i taskCount; i) { new Thread(() - { // 执行子任务 latch.countDown(); }).start(); } latch.await(); // 等待所有子任务完成 System.out.println(所有任务完成);11.2 CyclicBarrier让一组线程相互等待CyclicBarrier 强调“一等多”的栅栏效果一组线程互相等待直到所有线程都达到栅栏点后再一起继续执行。与 CountDownLatch 不同CyclicBarrier 的计数器可以重置适合分轮次重复使用的场景。javaint threadCount 3; CyclicBarrier barrier new CyclicBarrier(threadCount, () - { System.out.println(所有线程已到达栅栏点); }); for (int i 0; i threadCount; i) { new Thread(() - { try { System.out.println(Thread.currentThread().getName() 到达栅栏); barrier.await(); // 等待其他线程 System.out.println(Thread.currentThread().getName() 继续执行); } catch (Exception e) { Thread.currentThread().interrupt(); } }).start(); }11.3 Semaphore控制并发访问数量Semaphore 维护一组许可线程通过 acquire() 获取许可通过 release() 释放许可。只有拿到许可的线程才能继续执行从而实现限流、资源池控制等场景。javaSemaphore semaphore new Semaphore(2); // 最多允许 2 个线程同时访问 for (int i 0; i 5; i) { new Thread(() - { try { semaphore.acquire(); // 获取许可 System.out.println(Thread.currentThread().getName() 获得许可); Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { semaphore.release(); // 释放许可 } }).start(); }11.4 Exchanger两个线程之间的数据交换Exchanger 用于两个线程之间交换数据当一个线程调用 exchange() 后会阻塞等待另一个线程也调用 exchange()两者交换数据后各自继续执行。十二、CompletableFuture异步编程利器CompletableFuture 是 JDK 8 引入的异步编程工具它弥补了 Future 无法编排、无法组合、无法回调的缺陷让异步任务可以像 Stream 一样链式组合。12.1 创建异步任务java// 无返回值的异步任务 CompletableFutureVoid future CompletableFuture.runAsync(() - { System.out.println(异步执行任务); }); // 有返回值的异步任务 CompletableFutureString future2 CompletableFuture.supplyAsync(() - { return 结果; });12.2 链式编排javaCompletableFutureString future CompletableFuture .supplyAsync(() - hello) .thenApply(s - s world) .thenApply(String::toUpperCase); System.out.println(future.get()); // HELLO WORLD12.3 组合多个异步任务javaCompletableFutureString f1 CompletableFuture.supplyAsync(() - 任务1); CompletableFutureString f2 CompletableFuture.supplyAsync(() - 任务2); // 两个任务都完成后合并结果 CompletableFutureString combined f1.thenCombine(f2, (r1, r2) - r1 r2); System.out.println(combined.get()); // 任务1 任务2 // 任一任务完成就返回 CompletableFutureString any f1.applyToEither(f2, r - r); System.out.println(any.get());12.4 异常处理javaCompletableFutureString future CompletableFuture .supplyAsync(() - { if (true) throw new RuntimeException(模拟异常); return 正常结果; }) .exceptionally(ex - 兜底值 ex.getMessage()); System.out.println(future.get()); // 兜底值java.lang.RuntimeException: 模拟异常十三、总结JUC 是 Java 并发编程的核心工具库掌握它需要从底层原理到实战应用层层深入。回顾全文可以把核心结论压缩成下面几句话三大基石要理解可见性、原子性、有序性是一切并发问题的根源volatile 解决可见性和有序性CAS 和锁解决原子性。volatile 不等于原子性只保证可见性和禁止重排序复合操作仍需 AtomicInteger 或加锁。CAS 是无锁并发的根基所有原子类和 AQS 都建立在 CAS 之上但要注意 ABA 问题、自旋开销和单变量限制。AQS 是锁体系的灵魂state FIFO 队列 模板方法模式支撑了 ReentrantLock、Semaphore、CountDownLatch 等组件。ReentrantLock 比 synchronized 更灵活支持可中断、可超时、公平锁、多条件队列但必须手动释放锁。读写锁提升读并发读读共享、读写互斥、写写互斥锁降级可保证数据可见性StampedLock 进一步优化读多写少场景。并发集合比同步容器更高效ConcurrentHashMap、CopyOnWriteArrayList、BlockingQueue 都是生产环境的首选。线程池必须手动创建不要用 Executors明确 corePoolSize、maximumPoolSize、workQueue、拒绝策略避免 OOM。同步工具类让协作更简单CountDownLatch 一等多、CyclicBarrier 多等一、Semaphore 限流、Exchanger 交换数据。CompletableFuture 让异步编程更优雅链式组合、异常处理、多任务编排是 Future 的强力升级。
返回列表