ThreadPoolExecutor 源码解析

ThreadPoolExecutor 源码解析:Worker 机制、生命周期、拒绝策略与动态线程池实践 🚀 道格·李为什么需要一个线程池 Java 1.0 就支持多线程,但管理线程生命周期这件事一直缺少标准方案。开发者每次需要异步执行时,要么 new Thread().start(),要么自己维护一个线程管理队列——前者浪费资源,后者极易出错。 一个线程的创建和销毁是有成本的。JVM 要为每个线程分配栈内存(默认约 1MB),操作系统要为每个线程维护内核线程表项和调度上下文。当并发请求量上来后,频繁创建/销毁线程会导致: 内存压力——大量线程的栈内存吃掉堆外空间 CPU 浪费在上下文切换——线程数远超 CPU 核心数时,CPU 的时间片都消耗在"换人"而不是"干活"上 线程数不可控——请求峰值时线程数无上限增长,最终 OOM 或系统不可用 道格·李在设计 JSR 166 时面对的核心问题是:如何让开发者既能享受多线程的并发收益,又不用直接管理线程的创建和销毁? 答案是把线程抽象为一种可复用的资源——线程池。 线程池的本质是一个"线程 + 任务队列"的组合:核心线程常驻,任务多时创建临时线程分担,任务少时回收空闲线程,任务太多时由拒绝策略兜底。从设计上看,ThreadPoolExecutor 把线程的创建策略(core/max)、存活策略(keepAliveTime)、排队策略(workQueue)和过载策略(rejectedExecutionHandler)全部暴露为可配置参数——这正是道格·李的设计风格:不替开发者做决定,而是把决策权交给调用方。 📐 七大核心参数 ThreadPoolExecutor 最完整的构造器接受 7 个参数: public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) ⚙️ 1. corePoolSize — 核心线程数 线程池中始终存活的线程数量(除非 allowCoreThreadTimeOut 设为 true)。即使这些线程当前空闲,也不会被回收。 关键行为:当提交任务时,即使有空闲的核心线程,只要当前线程数少于 corePoolSize,线程池也会继续创建新的线程——先凑够核心线程数量,再谈复用。这种"先扩容再复用"是出于设计上的简单性:判断是否达到核心线程数的开销远小于判断是否有空闲线程且空闲线程是否可用。 📐 2. maximumPoolSize — 最大线程数 线程池允许创建的最大线程数。只有当工作队列已满且当前线程数不足 maximumPoolSize 时,才会创建超出核心线程数的额外线程。 ...

九月 2, 2022 · 20 分钟 · 4131 字 · yaomingye

ForkJoinPool 源码深度解析

ForkJoinPool 源码深度解析:从分治思想到工作窃取的完整实现 🤔 一、道格·李为什么需要一个"能偷工作"的线程池 Java 5 的 ThreadPoolExecutor 解决了线程复用的问题,但它有一个结构性的局限:所有线程共享一个任务队列。当一个线程提交了子任务后阻塞等待子任务结果,而子任务又在同一个队列里等待被执行时,就会发生线程饥饿——等待的线程占着一个槽位但不干活,队列里的子任务没人执行,形成死锁。 这个问题在递归分治算法(把大问题拆成小问题递归求解)中尤为致命。分治算法天然适合并行——子问题之间互不依赖,可以同时计算。但如果每个线程都把子任务扔到共享队列然后等结果,队列很快就会堆满等待被执行的任务而所有线程都在等。 道格·李在 Java 7 中引入 ForkJoinPool 时,核心创新是工作窃取(Work-Stealing): 每个工作线程有自己的双端队列(Deque),线程从自己的队列头部取任务 当一个线程 fork 子任务时,子任务被 push 到该线程自己的队列 当线程自己的队列空了,它会从其他线程的队列尾部窃取任务来执行 这个设计解决了两个问题:① 递归 fork 的子任务不会堵塞共享队列;② 快线程不会空等——它会偷慢线程的活来干。ForkJoinPool 也是 Java 8 并行流(parallelStream())的底层引擎。 二、设计思想:分治算法 + 工作窃取 📌 2.1 分治算法(Divide-and-Conquer) ForkJoinPool 的设计基础是分治算法(Divide-and-Conquer),其核心过程为三个方面: flowchart TD %% 半暗底色 + 高亮描边:完美适配博客深色/浅色双主题 %% classDef process fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#e5e7eb; classDef root fill:#0f172a,stroke:#3b82f6,stroke-width:2.5px,color:#bfdbfe,font-weight:bold; ROOT[根任务\n问题规模N] ROOT -->|拆分| L1[子任务\n规模N/2] ROOT -->|拆分| R1[子任务\n规模N/2] L1 -->|继续拆分| L2[子任务\n规模N/4] L1 -->|继续拆分| R2[子任务\n规模N/4] R1 -->|继续拆分| L3[子任务\n规模N/4] R1 -->|继续拆分| R3[子任务\n规模N/4] L2 -->|达到阈值\n直接计算| SOLVE1[原子任务] R2 -->|达到阈值\n直接计算| SOLVE2[原子任务] L3 -->|达到阈值\n直接计算| SOLVE3[原子任务] R3 -->|达到阈值\n直接计算| SOLVE4[原子任务] SOLVE1 -->|合并| MERGE1[汇总结果] SOLVE2 -->|合并| MERGE1 SOLVE3 -->|合并| MERGE2[汇总结果] SOLVE4 -->|合并| MERGE2 MERGE1 -->|最终合并| FINAL[最终结果] MERGE2 -->|最终合并| FINAL class FINAL,L1,L2,L3,MERGE1,MERGE2,R1,R2,R3,SOLVE1,SOLVE2,SOLVE3,SOLVE4 process; class ROOT root; 阶段 操作 说明 Divide(拆分) fork() 将大任务递归拆分为小任务,直到达到阈值 Conquer(求解) compute() 对原子任务执行实际计算 Combine(合并) join() 递归汇总所有子任务的结果 📌 2.2 工作窃取算法(Work-Stealing) 普通的 ThreadPoolExecutor 使用单一共享阻塞队列(BlockingQueue),所有线程竞争同一个队列的头元素,存在单点竞争瓶颈。ForkJoinPool 则采用完全不同的设计:每个工作线程维护自己的双端队列(WorkQueue)。 ...

九月 2, 2022 · 20 分钟 · 4180 字 · yaomingye

BlockingQueue 设计解析:四组方法语义、锁机制分化与生产者-消费者模型的工程实践

BlockingQueue 设计解析 🤔 道格·李为什么需要一个阻塞队列接口 生产者-消费者模式是多线程编程里最常见的协作模型——一个(或多个)线程生产数据,另一个(或多个)线程消费数据。在 JUC 出现之前,Java 开发者只能用 wait() / notify() 手写这个模型。 手写版本的典型代码如下: public synchronized void put(E e) throws InterruptedException { while (list.size() == capacity) { wait(); } list.addLast(e); notifyAll(); } 这段代码表面正确,但道格·李在分析并发程序的常见错误时发现了几个根深蒂固的问题: 生产者唤醒生产者:notifyAll() 唤醒等待队列里的所有线程——包括生产者和消费者。当队列满时,多个生产者同时被唤醒,只有第一个能成功插入,其余又回到 wait。这些"无效唤醒"不是 Bug,但大量浪费 CPU 无法区分等待原因:所有线程在同一个条件队列上等待,生产者因为"队列满"而等,消费者因为"队列空"而等。notifyAll() 叫醒所有人,但被叫醒的线程可能发现条件仍不满足,继续睡——这就是为什么 wait() 必须放在 while 循环里 没有标准接口:每个项目都在重新发明这个轮子,而且各自的行为语义不一致——有的用 null 表示失败,有的抛异常,有的阻塞等待 道格·李的解决方案是两层的:接口层——BlockingQueue 接口定义了四组标准方法(抛异常、返回特殊值、阻塞、超时),统一了所有阻塞队列的行为契约。实现层——用 ReentrantLock 的两个 Condition(notFull 和 notEmpty)精确分离生产者与消费者的等待条件,让"队列满"只唤醒消费者,“队列空"只唤醒生产者,消除无效唤醒。 🚧 BlockingQueue 接口设计:四组方法的语义定义 BlockingQueue 接口最核心的设计决策在于: 同一操作提供四种不同的线程协作策略 ,以方法名区分行为,以返回类型区分语义。 行为模式 插入 移除 检查 语义 抛异常 add(e) remove() element() 操作无法立即执行时抛出 IllegalStateException ,调用方需自行处理 返回特殊值 offer(e) poll() peek() 操作无法立即执行时返回 false 或 null ,调用方通过返回值判断是否成功 阻塞 put(e) take() — 操作无法立即执行时阻塞当前线程,直到条件满足,调用方被挂起 超时 offer(e, t, u) poll(t, u) — 操作无法立即执行时阻塞最多指定时长,超时返回 false 或 null 四组方法的核心设计哲学是"让调用方选择等待策略而非被动接受”。 同一个"放入元素"的需求,调用方可以根据业务场景选择: ...

八月 31, 2022 · 9 分钟 · 1713 字 · yaomingye

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
Cat Radio