FutureTask 源码深度解析

FutureTask 源码深度解析:从 Runnable 的局限到异步结果获取的完整实现 🤔 一、道格·李为什么需要一个"身兼两职"的任务对象 Java 的 Thread 构造函数接受 Runnable,但 Runnable.run() 返回值是 void——执行完就完了,拿不到结果。在 Java 1.0 ~ 1.4 时代,想在主线程拿到子线程的计算结果,只能靠共享变量(比如把结果写进一个 final int[] result = new int[1]),这种写法没有类型安全,也无法向调用方传递异常。 道格·李在 Java 5 的 JSR 166 中为这个问题设计了三个层次: 第一层:Callable<V>——任务接口。和 Runnable 功能等价,但 call() 有返回值且可抛异常。解决了"任务有结果"的问题。 第二层:Future<V>——结果句柄。提供 get()(阻塞获取结果)、cancel()(取消任务)、isDone()(判断完成)等方法。解决了"怎么拿到异步结果"的问题。但它只是一个接口,不知道任务在哪执行、怎么执行。 第三层:FutureTask——把两者粘在一起。它同时实现了 RunnableFuture<V> 接口(该接口同时继承 Runnable 和 Future),所以一个 FutureTask 对象既是可执行的任务(可以传给 Thread 或提交给 Executor),又是可查询的结果句柄(可以 get() 拿结果、cancel() 取消)。 道格·李这个设计的精巧之处在于:通过 FutureTask 这个"桥梁",ExecutorService.submit(Callable) 可以把任意 Callable 包装成 FutureTask,提交到线程池执行后立即返回 Future 句柄——调用方拿到了一个"未来的结果承诺",可以继续干别的事,需要结果时再 get()。 🔮 二、类继承体系:RunnableFuture 的双重身份 🏗️ 2.1 继承结构图 flowchart LR %% 半暗底色 + 高亮描边:完美适配博客深色/浅色双主题 %% classDef reject fill:#450a0a,stroke:#dc2626,stroke-width:2px,color:#fecaca,font-weight:bold; classDef process fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#e5e7eb; R[Runnable\nvoid run] F[Future\nget/cancel/isDone] RF[RunnableFuture\n同时继承Runnable+Future] FT[FutureTask] C[Callable\nV call throws Exception] R --> RF F --> RF RF --> FT C -->|组合-而非继承| FT class F,FT,R,RF process; class C reject; 接口/类 角色 核心方法 Runnable 可执行任务 void run() Callable<V> 有结果的任务 V call() throws Exception Future<V> 结果句柄 get(), cancel(), isDone() RunnableFuture<V> 二者的桥接接口 继承 Runnable + Future,无新增方法 FutureTask<V> 具体实现 组合 Callable,实现所有逻辑 📌 2.2 RunnableFuture 接口 public interface RunnableFuture<V> extends Runnable, Future<V> { void run(); } 这个接口只有 3 行,没有新增任何方法,只是将 Runnable 和 Future 合并。它的价值在于类型层面的统一——一个 RunnableFuture 实例可以同时作为任务提交给线程池和作为 Future 供调用方查询结果。 ...

八月 31, 2022 · 11 分钟 · 2292 字 · yaomingye

ConcurrentLinkedQueue

ConcurrentLinkedQueue:无锁并发队列、Michael-Scott 算法与 HOPS 延迟更新机制全解析 🚀 道格·李为什么需要一个无锁队列 生产环境中大量使用多生产者-多消费者的队列模型:多个线程投递任务,多个线程取出执行。传统的做法是用 LinkedList 加 synchronized——任意时刻只有一个线程能操作队列,其他线程排队等锁。在高并发下,锁争用(Lock Contention)迅速成为吞吐量瓶颈:CPU 时间大量消耗在线程的阻塞-唤醒切换上,而非实际的消息处理。 道格·李在设计 JSR 166 时为这个场景引入了一个完全不同的方案:无锁并发队列。ConcurrentLinkedQueue 使用 CAS(Compare-And-Swap,比较并交换)原子操作替代锁,基于 Michael-Scott 算法(1996 年由 Maged Michael 和 Michael Scott 提出的无锁队列算法)实现多线程并发入队和出队。 核心设计思想: 入队和出队操作各自独立——生产者在队尾 CAS 插入,消费者在队头 CAS 移除,彼此不阻塞 没有锁,就不会有线程被操作系统挂起——CAS 失败意味着有其他线程抢先了一步,重试即可,没有上下文切换开销 HOPS 延迟更新策略——tail 指针不必每次都更新到最后一个节点,允许滞后 1 ~ 2 个位置,用额外的 CAS 判断换来更少的 volatile 写操作 ConcurrentLinkedQueue 是 JUC 无锁数据结构的基础模型——理解了它的 CAS 操作模式和 HOPS 策略,再看 ConcurrentHashMap、LinkedTransferQueue 等无锁结构会轻松很多。 🏗️ 核心数据结构 整体架构:基于 Node 的单向链表 ConcurrentLinkedQueue 的底层是一个 单向链表,由 head 和 tail 两个 volatile 指针维护: ...

八月 28, 2022 · 12 分钟 · 2547 字 · yaomingye

ConcurrentHashMap 深度解析:数据结构、线程安全与扩容机制

ConcurrentHashMap 深度解析:从数据结构到线程安全的底层实现 一、道格·李为什么需要重新设计一个并发哈希表 Java 1.0 提供了 Hashtable——一个线程安全的 Map 实现。它的线程安全策略很简单:在所有 public 方法上加 synchronized。这个策略正确但不实用——任何时候只有一个线程能操作整个表,即使两个线程操作的是不同的键。在 1.0 时代并发不常见时还凑合,到了 Java 5 时代,服务器端的多线程访问同一个缓存 Map 已经是常规操作,Hashtable 的全局锁成了吞吐量的天花板。 HashMap 是 Hashtable 的非线程安全替代,性能好得多,但一旦多线程并发 put,就会出现数据丢失、size 计数错误,甚至在 JDK 7 扩容时出现链表成环导致 CPU 100%。 道格·李在设计 ConcurrentHashMap 时面临的问题是:既要保证线程安全(不能丢数据),又要提供接近 HashMap 的并发吞吐量(不能全局锁)。这是两个互相矛盾的目标,传统的 synchronized 方案只能取其一。 道格·李的解决方案是把锁的粒度从"整张表"缩小到"单个桶"。JDK 5 ~ 7 中用了 Segment 分段锁(16 个段,每段独立加锁),JDK 8 进一步细化为桶级别 CAS + synchronized——对空桶用 CAS 无锁插入,对非空桶只锁链表/红黑树的头节点。这种设计让 16 个线程同时操作 16 个不同桶时完全无竞争,并发度从 Hashtable 的 1 提升到桶的数量级。 🗺️ 二、ConcurrentHashMap 的数据结构 JDK 8 的 ConcurrentHashMap 放弃了 JDK 7 的 Segment 分段锁设计,直接采用与 HashMap 相似的结构: Node 数组 + 链表 + 红黑树 。 ...

八月 27, 2022 · 15 分钟 · 3061 字 · yaomingye

CopyOnWriteArrayList 源码深度解析

CopyOnWriteArrayList 源码深度解析:从线程安全列表到写时复制的实现原理 一、道格·李为什么需要"写时复制"的列表 Java 1.0 的 Vector 用 synchronized 保证线程安全——所有方法加锁,读写都互斥。Collections.synchronizedList 同理。这在读多写少的场景中有一个严重的浪费:多个线程同时读取不应该互相阻塞,因为读取不修改数据。 但去掉锁也不行——ArrayList 的迭代器有 fail-fast 机制,遍历时如果有其他线程写入,直接抛 ConcurrentModificationException。而且多线程同时 add() 还会导致数据丢失(elementData 数组和 size 计数器都没有同步保护)。 道格·李在 JSR 166 中为这个场景设计了一个完全不同的策略:写时复制(Copy-On-Write)。每次写入(add、set、remove)不直接修改原数组,而是复制一份新数组,在新数组上操作,最后用 volatile 写把引用指向新数组。读操作完全无锁——直接读当前的数组引用,不需要任何同步。 这个设计的取舍非常明确:写操作很贵(要复制整个数组),但读操作极其便宜(无锁 + volatile 读)。因此 CopyOnWriteArrayList 仅适用于读多写极少(比如读:写 > 100:1)的场景——配置信息、监听器列表、白名单等写入很少但频繁遍历的数据结构。 📐 二、设计理念:Copy-On-Write 📌 2.1 什么是写时复制 写时复制(Copy-On-Write,COW)是一种并发优化策略。它的核心思想只有一句话:当容器需要被修改时,不直接在原数组上操作,而是先复制一份新数组,在新数组上修改,修改完成后用新数组替换旧数组的引用。 这就保证了:读操作永远在不变的数组上进行,完全不需要加锁;写操作虽然开销大,但只影响它自己,不会阻塞任何读线程。 📌 2.2 CopyOnWriteArrayList 的架构总览 CopyOnWriteArrayList 的并发安全由"一锁一数组"两个组件配合完成: flowchart TD %% 半暗底色 + 高亮描边:完美适配博客深色/浅色双主题 %% classDef process fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#e5e7eb; COW[CopyOnWriteArrayList] COW --> ARRAY["array: volatile Object[]"] COW --> LOCK["lock: ReentrantLock"] ARRAY --> A0["元素0"] ARRAY --> A1["元素1"] ARRAY --> A2["元素2"] ARRAY --> AN["元素N"] LOCK --> W["写线程"] ARRAY --> R["读线程"] W --> |写操作加锁| LOCK R --> |volatile读无锁| ARRAY class A0,A1,A2,AN,ARRAY,COW,LOCK,Object,R,W process; 组件 类型 作用 array volatile Object[] 存储元素,volatile 保证写后对其他线程立即可见 lock final ReentrantLock 写操作的互斥锁,同一时刻只允许一个写线程 array 加上 volatile 是关键——写线程完成数组替换后,volatile 写语义将所有读线程看到的旧引用刷成新引用,保证最终一致性。 ...

八月 27, 2022 · 9 分钟 · 1854 字 · yaomingye

Semaphore 源码解析

Semaphore 源码解析:AQS 共享模式、许可传播机制与公平策略实现 🚀 道格·李为什么需要一个信号量 信号量(Semaphore)是操作系统教科书里最古老的并发原语之一,由 Edsger Dijkstra 在 1960 年代提出。但在 Java 1.0 ~ 1.4 时代,JDK 里并没有信号量——开发者只能用 synchronized 加一个计数器模拟,代码又长又容易出错。 道格·李在设计 JSR 166 时,需要将信号量引入 Java,原因很简单:synchronized 是互斥的(同一时刻只能一个线程进入),而很多并发控制场景需要的是"限制并发数量"而不是"限制到只有 1 个"。比如数据库连接池最多 10 个并发连接、文件读取最多 3 个线程同时打开、API 限流每秒 100 个请求——这些场景用 synchronized 无法表达。 Semaphore 的思路是许可计数:构造时定义 N 个"许可证",线程调用 acquire() 拿走一个许可(不够就阻塞),用完调用 release() 归还。许可与线程没有绑定关系——线程 A 获取的许可可以由线程 B 释放。这个设计很关键:它让 Semaphore 不仅可以用作"限流器",还可以用作"对象池管理器"或"同步器"。 在 AQS 框架中,Semaphore 使用的是共享模式(Shared Mode)——多个线程可以同时获取许可,不像 ReentrantLock 的独占模式那样一次只唤醒一个线程。 🏗️ 核心数据结构 🏗️ 整体类层次结构 Semaphore 和 ReentrantLock 的内部结构高度相似——都是通过内部类 Sync 间接继承 AQS,并通过两个子类实现公平/非公平策略。但有一个关键区别:Semaphore 使用的是 AQS 的 共享模式(Shared Mode),而非独占模式。 ...

八月 26, 2022 · 10 分钟 · 2051 字 · yaomingye

CountDownLatch 设计思想解析:一次性的共享门闩为什么这样设计

CountDownLatch:一次性的共享门闩 🤔 道格·李为什么需要一个倒计时门闩 多线程编程里有一个反复出现的问题:主线程需要等待若干个子任务全部完成,然后汇总结果继续执行。在 JUC 出现之前,Java 只有两种方式应对: Thread.join()——但 join() 等的是线程终止,不是任务完成。如果线程来自线程池(被复用,不会终止),join() 完全不适用。它是"等人死了"而不是"等人把活干完"。 自己写自旋等待——用一个 volatile 计数器,主线程循环检查。但自旋空转浪费 CPU,加 sleep 又会引入延迟,而且 count++ 本身不是原子操作。 道格·李在设计 JSR 166 时看到了这个空缺:需要一个轻量级的同步辅助工具,让一个(或多个)线程能够等待其他线程完成一组操作,不依赖线程终止,不浪费 CPU,而且足够简单。 这就是 CountDownLatch 的诞生背景。它用 AQS 的共享模式实现了一个一次性的倒计时器:计数器从 N 开始,每个子任务完成时减 1(countDown()),主线程在 await() 上阻塞直到计数器归零。 道格·李给它设计了几个刻意的约束: 一次性——计数器归零后无法重置。这个约束简化了实现(不需要考虑"重置时正在等待的线程怎么办"),也迫使使用者为可重复场景选用 CyclicBarrier 只减不增——countDown() 只能减少计数,无法增加。这也简化了状态机——计数器状态只有 N→0 这一个方向 基于 AQS 共享模式——让多个等待线程可以同时被唤醒(当计数器归零时),而不是排他锁那样只唤醒一个 设计理念 1:一次性的约束 CountDownLatch 最核心的设计约束是 一次性 ——一旦计数器从 N 减到 0,门闩永久打开,无法再关闭。 这个约束绝非"能力不足",而是 刻意的设计取舍 : 如果支持重置 代价 需要处理"已有线程在 await 上等待"和"重置后的新等待者"两种状态 状态机复杂度翻倍 countDown 和 reset 并发时的语义难以定义 需要额外的同步机制 每个等待者都要知道自己是"老批次"还是"新批次" 需要代际(generation)标记 一次性约束消除了一个巨大的设计空间:时间维度上的状态管理。 CountDownLatch 只有两个有意义的状态: state > 0 (门闩关闭)和 state == 0 (门闩打开)。从关闭到打开只需要 state 单向递减,不需要考虑"打开了又关上"的复杂路径。 ...

八月 26, 2022 · 5 分钟 · 986 字 · yaomingye

ReentrantReadWriteLock 深度解析:从读多写少场景到底层 state 拆分与锁降级

ReentrantReadWriteLock 深度解析 🤔 道格·李为什么要区分读锁和写锁 ReentrantLock 是可重入的互斥锁——无论什么操作,同一时刻只有一个线程能持有锁。这在写多读少的场景里不是问题,但在读多写少的场景(缓存查询、配置读取、字典加载)中,ReentrantLock 带来了不必要的串行化:100 个线程同时读,它们不应该互相阻塞,因为读取不修改数据。 道格·李在设计 JSR 166 时专门为此引入了读写锁。ReentrantReadWriteLock 把锁拆成两把: 读锁(readLock)——共享锁,多个读线程可以同时持有,读与读不互斥 写锁(writeLock)——独占锁,写线程独占,写与写、写与读互斥 这个设计基于一个观察:大多数并发访问的数据结构是"读多写少"的。如果 90% 的操作都是读取,那么互斥锁让这 90% 的操作全部串行化——白白浪费了并发能力。读写锁让读操作完全并行,只在写操作发生时短暂阻塞。 内部实现上,ReentrantReadWriteLock 把 AQS 的 32 位 state 字段拆成高 16 位(记录共享的读锁持有次数)和低 16 位(记录独占的写锁重入次数),用一个 int 同时跟踪两种模式的持有状态。这也是为什么读写锁在同一时刻要么被读线程共享、要么被写线程独占——两种模式共享同一个 state,状态机决定了它们互斥。 📋 基本用法:readLock 和 writeLock ReentrantReadWriteLock 提供两个 Lock 对象: 锁 类型 获取方法 AQS 模式 readLock() 共享锁 readLock.lock() 共享模式(Shared) writeLock() 独占锁 writeLock.lock() 独占模式(Exclusive) ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); Lock readLock = rwLock.readLock(); Lock writeLock = rwLock.writeLock(); // 读锁:多线程可同时持有 readLock.lock(); try { // 读取操作 } finally { readLock.unlock(); } // 写锁:只有一个线程能持有 writeLock.lock(); try { // 写入操作 } finally { writeLock.unlock(); } 读写互斥规则 stateDiagram-v2 state "无锁" as NOLOCK state "读锁持有中" as READ state "写锁持有中" as WRITE NOLOCK --> READ : 任意线程获取读锁 NOLOCK --> WRITE : 任意线程获取写锁 READ --> READ : 其他线程获取读锁(允许) READ --> WRITE : 任意线程获取写锁(阻塞等待) WRITE --> READ : 任意线程获取读锁(阻塞等待) WRITE --> WRITE : 任意线程获取写锁(阻塞等待) READ --> NOLOCK : 所有读锁释放 WRITE --> NOLOCK : 写锁释放 当前状态 请求读锁 请求写锁 无锁 允许 允许 读锁持有中 允许(读-读不互斥) 阻塞(读-写互斥) 写锁持有中 阻塞(写-读互斥) 阻塞(写-写互斥) 关键点 :写锁释放后,如果有读线程和写线程同时在等待,谁先获取取决于锁的公平性设置。非公平模式下,写线程可能被插队的读线程持续阻塞(写饥饿问题)。 ...

八月 25, 2022 · 8 分钟 · 1648 字 · yaomingye

ReentrantLock 源码解析

ReentrantLock 源码解析:AQS 同步队列、可重入机制与公平锁实现 🚀 道格·李为什么需要一把新锁 在 Java 1.0 时代,多线程互斥只有一个选择——synchronized。它用起来简单:在方法签名上加个关键字,JVM 自动处理加锁和解锁。但随着并发编程场景的复杂化,synchronized 的局限越来越明显: 无法尝试获取锁:线程要么拿到锁,要么无限期阻塞。没有"试一下,拿不到就先干别的"这个选项。这在需要获取多个锁(避免死锁)的场景里是致命的——一旦第一个锁拿到但第二个锁拿不到,已持有的锁无法自动释放 无法中断等待:如果一个线程在 synchronized 上阻塞了,外部无法通过 interrupt() 让它停止等待。这在需要超时取消的场合(比如用户点了取消按钮)完全没办法 无法实现公平锁:synchronized 的锁分配由 JVM 内部机制决定,不保证先来后到。高并发下可能出现线程饥饿——某个线程永远抢不到锁 一个对象只有一个条件队列:synchronized 配合 wait/notify 使用时,所有线程在同一个 wait set 上等待,无法区分"因为缓冲区满了而等待的生产者"和"因为缓冲区空了而等待的消费者" 道格·李在设计 JSR 166(java.util.concurrent 包的基础)时意识到:要构建一个可靠的并发工具包,必须有一把比 synchronized 更灵活的锁。这把锁需要支持尝试获取、超时获取、可中断获取、公平调度——这些 synchronized 做不到的事,是构建 Semaphore、CountDownLatch、BlockingQueue 这些高级并发组件的基础。 这就是 ReentrantLock 的诞生背景。它不是简单地把 synchronized 重写一遍,而是把锁的控制权从 JVM 内部暴露给开发者——开发者可以决定:要不要公平、等多久算超时、拿到锁之后要不要释放。 // synchronized 做不到的三件事: // ① tryLock:试一下,拿不到就做别的 if (lock.tryLock()) { try { ... } finally { lock.unlock(); } } // ② tryLock(timeout):等一段时间,超时就不等了 if (lock.tryLock(2, TimeUnit.SECONDS)) { try { ... } finally { lock.unlock(); } } // ③ lockInterruptibly:别人让你停你就停 lock.lockInterruptibly(); // 被 interrupt 时抛 InterruptedException 接下来逐层深入 ReentrantLock 的内部实现——它如何在 AQS 框架上构建这些能力。 ...

八月 24, 2022 · 13 分钟 · 2617 字 · yaomingye

LockSupport 深度解析:从 wait/notify 的痛点到底层 park/unpark 实现

LockSupport 深度解析 🤔 道格·李为什么需要一个比 wait/notify 更可靠的阻塞原语 在 java.util.concurrent 诞生之前,Java 线程阻塞/唤醒的唯一手段是 Object.wait() 和 Object.notify()。每个 Java 程序员都知道这两件事:第一,必须在 synchronized 块里调用;第二,notify() 如果在 wait() 之前调用,信号就丢了,线程永远醒不过来。 道格·李在构建 AQS 时遇到了一个棘手的问题:AQS 的 acquire() 是先尝试获取锁,失败了再 park()。但线程可能在 tryAcquire 失败和 park() 之间被 unpark()——如果 park() 没有"许可证记忆"能力,这个 unpark() 就白调了,线程永久阻塞。这和 wait/notify 的丢信号问题是同源的。 更麻烦的是,wait/notify 必须配合 synchronized 使用,而 AQS 内部用的是 CAS——如果为了调 wait() 还要加一层 synchronized,性能和设计都会变成灾难。 道格·李需要一个更底层的线程阻塞原语,满足三个条件:unpark() 可以先于 park() 调用(许可证语义,不像 notify 必须后于 wait)、不需要配合 synchronized 监视器锁、直接调用操作系统的线程挂起/恢复能力。 这就是 LockSupport 的诞生背景。它基于二值信号量(permit)——每个线程有且仅有一个 permit(0 或 1),park() 消费 permit(没有则阻塞),unpark() 生产 permit(最多为 1)。这一设计让 AQS 的 acquire() / release() 有了可靠的线程调度基础。 ...

八月 22, 2022 · 7 分钟 · 1280 字 · yaomingye

CAS 从硬件到 Java:MESI 视角下的原子操作与 12 个原子类

CAS 从硬件到 Java 🤔 一、Intel 的工程师为什么要给 CPU 加一条 lock cmpxchg 指令 多线程编程中最基础的问题——count++ 不是原子操作。Java 层面它是三条字节码,CPU 层面它是 “LOAD → ADD → STORE” 三条指令。两个核心同时执行,结果必然互相覆盖。 一种解决思路是加锁——synchronized 把整个 count++ 包住,一次只有一个线程执行。但锁的代价高:上下文切换、线程阻塞/唤醒、内核态切换。高竞争场景下,线程在等待锁上花的时间可能比干活的时间还多。 有没有办法不阻塞线程、靠硬件指令实现原子更新?Intel 的 CPU 架构师提供了一个答案:lock cmpxchg(Compare and Swap)指令。它将"比较旧值→如果匹配就写新值"这个过程变成一条不可分割的 CPU 指令,配合 lock 前缀锁定总线(或缓存行),保证同一时刻只有一个核心能成功操作该内存地址。 这个思路的妙处在于:把"锁"从软件层(JVM / OS Mutex)下沉到硬件层(CPU 缓存一致性协议)。失败重试的代价只是几个 CPU 周期,不死锁、不阻塞、不切换上下文。道格·李在 JUC 中大量依赖 CAS 来构建无锁数据结构——ConcurrentHashMap 的 bucket 写入、ConcurrentLinkedQueue 的节点插入、AQS 的 state 更新,底层全是 CAS。 本文从 CAS 的硬件原理开始,一直讲到 Java 的 12 个原子类。 🏗️ 二、MESI 视角:为什么硬件需要 CAS 🏗️ 2.1 两个 CPU 同时写一个变量——MESI 的"竞速" 从 MESI 协议的角度重新审视 i++ 的三条指令。假设两个 CPU 核心(Core A 和 Core B)同时尝试对同一地址执行 ++: ...

八月 21, 2022 · 9 分钟 · 1876 字 · yaomingye
Cat Radio