Java AQS 并发同步框架深度解析
Executive Summary
核心观点(金字塔原理)
结论先行: AQS 是 Java 并发包的基石,通过独占锁和共享锁两种模式支撑了 ReentrantLock、Semaphore 等核心同步工具,理解其原理是掌握 Java 并发编程的关键。
支撑论点:
- 锁的两种基本模式:独占锁(悲观锁)保证互斥访问,共享锁(乐观锁)支持读并发写互斥
- 自旋锁 vs 互斥锁的选择取决于锁持有时间:短时间用自旋锁效率高,长时间用互斥锁避免 CPU 空转
- 公平锁与非公平锁的权衡:公平锁保证顺序但吞吐量低,非公平锁允许插队但吞吐量高
SWOT 分析
| 维度 | 分析 |
|---|---|
| S 优势 | 提供统一的锁实现框架;支持独占和共享两种模式;可扩展性强 |
| W 劣势 | 学习曲线陡峭;自旋锁使用不当可能导致死锁或 CPU 浪费 |
| O 机会 | 高并发场景下性能优化;读多写少场景使用读写锁提升吞吐量 |
| T 威胁 | 递归获取自旋锁导致死锁;锁持有时间过长导致性能下降 |
适用场景
- 需要实现自定义同步器的场景
- 高并发读多写少场景(使用读写锁)
- 需要精细控制锁行为(公平/非公平)的业务场景
AQS (java.util.concurrent.locks.AbstractQueuedSynchronizer) JDK Version 1.8
- 独占锁:悲观锁,每次只能有一个线程持有锁,以独占的方式实现互斥锁
- 共享锁:乐观锁,允许多个线程同时获取锁,并发访问,允许一个写锁或者多个读锁。注意是或者不是且。
- 一次只有一个线程可以占有写模式的读写锁, 但是可以有多个线程同时占有读模式的读写锁. 正是因为这个特性,当读写锁是写加锁状态时, 在这个锁被解锁之前, 所有试图对这个锁加锁的线程都会被阻塞.当读写锁在读加锁状态时, 所有试图以读模式对它进行加锁的线程都可以得到访问权, 但是如果线程希望以写模式对此锁进行加锁, 它必须直到所有的线程释放锁.通常, 当读写锁处于读模式锁住状态时, 如果有另外线程试图以写模式加锁, 读写锁通常会阻塞随后的读模式锁请求, 这样可以避免读模式锁长期占用, 而等待的写模式锁请求长期阻塞.读写锁适合于对数据结构的读次数比写次数多得多的情况. 因为, 读模式锁定时可以共享, 以写模式锁住时意味着独占, 所以读写锁又叫共享-独占锁.
- 总结: 1. 当前为写锁,则后续不论读写都阻塞。2. 当前为读锁,则后续如果为读锁则可以得到访问权,若为写锁,则等待所有读锁释放才可以获取写锁并且同时阻塞后续的读写锁的请求,避免读锁长期占用,而等待的写锁请求长期阻塞。3. 读写锁适合读多写少的情况
- 互斥锁 & 自旋锁
- 自旋锁是专为防止多处理器并发而引入的一种锁,它在内核中大量应用于中断处理等部分(对于单处理器来说,防止中断处理中的并发可简单采用关闭中断的方式,即在标志寄存器中关闭/打开中断标志位,不需要自旋锁)。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在
调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,”自旋”一词就是因此而得名。自旋锁比较适用于锁使用者保持锁时间比较短的情况。正是由于自旋锁使用者一般保持锁时间非常短,因此选择自旋而不是睡眠是非常必要的,自旋锁的效率远高于互斥锁。信号量和读写信号量适合于保持时间较长的情况,它们会导致调用者睡眠,因此只能在进程上下文使用,而自旋锁适合于保持时间非常短的情况,它可以在任何上下文使用。事实上,自旋锁的初衷就是:在短期间内进行轻量级的锁定。一个被争用的自旋锁使得请求它的线程在等待锁重新可用的期间进行自旋(特别浪费处理器时间),所以自旋锁不应该被持有时间过长。如果需要长时间锁定的话, 最好使用信号量。当临界区很大或有共享设备的时候,需要较长时间占用锁,使用自旋锁会降低系统的性能。自旋锁可能导致系统死锁。引发这个问题最常见的情况是递归使用一个自旋锁,即如果一个已经拥有某个自旋锁的CPU 想第二次获得这个自旋锁,则该CPU 将死锁。此外,如果进程获得自旋锁之后再阻塞,也有可能导致死锁的发生。自旋锁只有在内核可抢占或SMP(多处理器)的情况下才真正需要,在单CPU且不可抢占的内核下,自旋锁的所有操作都是空操作。 - 锁的公平与非公平,是指线程请求获取锁的过程中,是否允许插队。在公平锁上,
线程将按他们发出请求的顺序来获得锁;而非公平锁则允许在线程发出请求后立即尝试获取锁,如果可用则可直接获取锁,尝试失败才进行排队等待。ReentrantLock提供了两种锁获取方式,FairSyn和NofairSync。结论:ReentrantLock是以独占锁的加锁策略实现的互斥锁,同时它提供了公平和非公平两种锁获取方式。最初看源码时竟然把这两个概念弄混了。