摘要:本篇文章围绕 Java 并发编程中锁的核心内容,梳理了 Lock 接口与 synchronized 的差异,详解了队列同步器AQS的实现机制、ReentrantLock的可重入特性、公平锁与非公平锁的实现。
第5章 Java 中的锁
5.1 Lock接口
"它提供了与 synchronized 关键字类似的同步功能,只是在使用时需要显式地获取和释放锁。虽然它缺少了(通过 synchronized 块或者方法所提供的)隐式获取释放锁的便捷性,但是却拥有了锁获取与释放的可操作性、可中断的获取锁以及超时获取锁等多种 synchronized 关键字所不具备的同步特性。"
这句话精准点明了Lock接口(以ReentrantLock为例)与synchronized的核心差异和价值。
ReentrantLock(Lock 接口实现)与 synchronized 的详细区别
| 特性 | ReentrantLock(Lock 接口实现) | synchronized |
| 获取 / 释放方式 | 显式调用 lock() / unlock(),必须在 finally 中释放锁,灵活性高 | 隐式获取 / 释放(JVM 自动管理),简化了同步管理,但无法灵活控制释放时机 |
| 可中断获取锁 | 支持 lockInterruptibly(),在等待获取锁时可响应中断,避免线程永久阻塞 | 不支持,一旦进入等待状态,只能等锁释放或线程终止 |
| 超时获取锁 | 支持 tryLock(long time, TimeUnit unit),在指定时间内未获取到锁则返回 false,避免无限等待 | 不支持,只能无限等待 |
| 非阻塞获取锁 | 支持 tryLock(),尝试获取锁,获取失败立即返回 false,不会阻塞 | 不支持,获取不到锁就会阻塞 |
| 锁获取顺序 | 可公平 / 非公平锁,公平锁严格遵循 FIFO 顺序,非公平锁默认插队抢占 | 非公平锁,默认不保证顺序 |
| 重入性 | 支持,重入次数通过 getHoldCount() 查询 | 支持,但无法查询重入次数 |
| 扩展性 | 可灵活控制锁的获取 / 释放顺序,适合复杂场景(如多锁顺序控制) | 锁的获取 / 释放顺序固化,扩展性差 |
三、核心差异总结
灵活性:
ReentrantLock以 "显式操作" 为代价,换取了synchronized不具备的可中断、超时获取、精准唤醒等特性,适合复杂并发场景;易用性:
synchronized由 JVM 自动管理,无需手动释放,降低了死锁风险,适合简单同步场景;性能:在高并发场景下,
ReentrantLock的性能通常优于synchronized(尤其是非公平锁);功能丰富度:
ReentrantLock支持公平锁、多个Condition、锁状态查询等高级功能,是synchronized的超集。
简单来说:
如果你的场景简单,用
synchronized更省心;如果需要处理复杂的锁控制(如超时等待、可中断、精准唤醒),用
ReentrantLock更强大。
5.2 队列同步器
"队列同步器 AbstractQueuedSynchronizer(以下简称同步器),是用来构建锁或者其他同步组件的基础框架,它使用了一个 int 成员变量表示同步状态,通过内置的 FIFO 队列来完成资源获取线程的排队工作。"
这句话是整段内容的核心,直接点明了 AQS 的本质、核心实现机制与定位。它清晰解释了 AQS 是什么(基础框架)、如何工作(用int维护状态、用 FIFO 队列管理线程排队)、能解决什么问题(构建各种同步组件)。
5.2.1 队列同步器的接口
同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态,在抽象方法的实现过程中免不了要对同步状态进行更改,这时就需要使用同步器提供的 3 个方法(getState ()、setState (int newState) 和 compareAndSetState (int expect,int update))来进行操作,因为它们能够保证状态的改变是安全的。
使用方式:AQS 采用 "模板方法" 设计模式,需要子类继承它,并实现特定的抽象方法(如独占式获取 / 释放、共享式获取 / 释放)来定义同步逻辑。
状态操作:修改同步状态时,必须使用 AQS 提供的
getState()、setState()、compareAndSetState()方法,这些方法能保证在多线程下对int状态的修改是线程安全的,尤其是compareAndSetState()是基于 CAS 实现的,能避免并发冲突。
表 5-3 列出了 AQS 中需要子类重写的核心方法,这些方法是实现自定义同步逻辑的关键,分为独占式和共享式两类:
1.独占式方法(同一时刻仅一个线程持有)
protected boolean tryAcquire(int arg)作用:尝试独占式获取同步状态。
逻辑:查询当前同步状态,判断是否符合获取条件。若符合,就用
compareAndSetState()(CAS)修改状态,成功返回true,失败返回false。典型应用:
ReentrantLock中实现 "加锁" 的核心逻辑。
protected boolean tryRelease(int arg)作用:尝试独占式释放同步状态。
逻辑:修改同步状态,释放后会唤醒队列中等待的线程,让它们有机会获取同步状态。
典型应用:
ReentrantLock中实现 "解锁" 的核心逻辑。
2.共享式方法(同一时刻允许多个线程持有)
protected int tryAcquireShared(int arg)作用:尝试共享式获取同步状态。
逻辑:返回值为
>=0表示获取成功;返回值<0表示获取失败。典型应用:
CountDownLatch中 "等待" 的逻辑、ReentrantReadWriteLock的读锁获取逻辑。
protected boolean tryReleaseShared(int arg)作用:尝试共享式释放同步状态。
逻辑:释放同步状态,成功返回
true,并会唤醒所有等待的共享式线程。典型应用:
CountDownLatch中 "计数减一" 的逻辑、ReentrantReadWriteLock的读锁释放逻辑。
共享模式和独占模式的核心区别,在于获取成功后的行为,而不是获取前的检查:
独占模式:线程 B 成功获取后,只会把自己设为新的头节点,等待下一次释放时再唤醒它的后继。
共享模式:线程 B 成功获取后,除了把自己设为新的头节点,还会继续向后传播唤醒它的后继节点(线程 C),以此让多个线程能同时获取共享资源。
5.2.2 队列同步器的实现分析
1.同步队列的核心作用
AQS 内部维护一个FIFO 双向队列,这是它实现线程同步的基础。
当线程尝试获取同步状态失败时,会被包装成一个
Node节点,加入队列尾部并阻塞。当持有同步状态的线程释放资源时,会唤醒队列头部的线程,让它再次尝试获取同步状态。
节点(Node)的结构与作用
每个
Node节点保存了以下关键信息:线程引用:指向获取同步状态失败的线程。
等待状态:表示线程在队列中的等待状态(如
CANCELLED、SIGNAL、CONDITION等)。前驱 / 后继指针:用于维护队列的双向链表结构,支持节点的插入与移除。
这个设计确保了线程能以有序、安全的方式排队等待,避免了 "饥饿" 问题。
完整工作流程
线程获取状态失败→ 被包装成
Node加入队列尾部,并阻塞。线程释放状态→ 唤醒队列头部的线程。
被唤醒的线程→ 再次尝试获取同步状态,成功则从队列中移除,失败则继续等待。
当一个线程成功地获取了同步状态(或者锁),其他线程将无法获取到同步状态,转而被构造成为节点并加入到同步队列中,而这个加入队列的过程必须要保证线程安全,因此同步器提供了一个基于 CAS 的设置尾节点的方法:
compareAndSetTail(Node expect, Node update)。
1.同步队列的基本结构
AQS 内部维护一个FIFO双向链表作为同步队列,包含两个核心引用:
head:指向队列的头节点(代表当前持有同步状态的线程)。tail:指向队列的尾节点(代表最后一个进入队列等待的线程)。
每个节点(
Node)包含prev(前驱指针)和next(后继指针),用于维护链表的双向关联。
2.节点入队的核心逻辑
触发条件:当线程尝试获取同步状态失败时,会被包装成一个
Node节点,准备加入队列。线程安全保证:为了在多线程并发入队时保证队列的一致性,AQS 使用
compareAndSetTail这个基于 CAS 的方法来设置新的尾节点。它需要传入两个参数:
expect(当前线程认为的尾节点)和update(当前要加入的新节点)。只有当 "预期的尾节点" 和实际尾节点一致时,才会将新节点设置为尾节点,并建立前驱关联,从而保证入队操作的原子性。
头节点更新:当队列头部的线程成功获取同步状态后,会调用
setHead方法将该节点设置为新的头节点,原头节点会被移除。
3.完整流程梳理
线程 A 成功获取同步状态,成为工作线程。
线程 B、C 等后续线程获取失败,被包装为
Node节点。这些节点通过
compareAndSetTail方法,以 CAS 安全地添加到队列尾部。当线程 A 释放同步状态时,会唤醒头节点的后继节点(线程 B),让它尝试获取同步状态。
线程 B 成功获取后,通过
setHead成为新的头节点,等待队列继续向后推移。
线程 A 成功获取同步状态,成为工作线程。
线程 B、C 等后续线程获取失败,被包装为
Node节点,调用enq方法尝试入队。在
enq方法的 死循环(自旋)中,线程通过compareAndSetTail以 CAS 方式安全地将自己设置为新的尾节点,只有设置成功才会退出循环,否则会一直尝试,从而将并发入队的请求 "串行化"。节点成功入队后,进入自旋等待阶段:每个节点会不断检查自己的前驱节点是否为头节点,以此判断自己是否有机会获取同步状态。
当线程 A 释放同步状态时,会唤醒头节点的后继节点(线程 B)。
线程 B 被唤醒后,在自旋中检查到自己的前驱是头节点,便会尝试获取同步状态。
线程 B 成功获取同步状态后,调用
setHead方法将自己设置为新的头节点,原头节点被移除,等待队列继续向后推移。如果线程 B 获取失败,会再次进入阻塞状态,等待下一次被唤醒。
5.3 重入锁 ReentrantLock
"该方法增加了再次获取同步状态的处理逻辑:通过判断当前线程是否为获取锁的线程来决定获取操作是否成功,如果是获取锁的线程再次请求,则将同步状态值进行增加并返回 true,表示获取同步状态成功。"
这句话精准点出了ReentrantLock可重入特性的核心实现逻辑,是理解这段代码的关键。
final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); // 1. 锁未被任何线程持有 if (c == 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } // 2. 锁已被当前线程持有(可重入逻辑) else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires;if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } // 3. 锁被其他线程持有,获取失败 return false; }为什么叫可重入锁:如果当前线程已经是锁的拥有者(current == getExclusiveOwnerThread()),则直接增加同步状态值(nextc = c + acquires),表示重入次数加一。
"如果该锁被获取了 n 次,那么前 (n-1) 次
tryRelease(int releases)方法必须返回 false,而只有同步状态完全释放了,才能返回 true。"
这句话精准概括了ReentrantLock可重入特性在释放锁时的核心逻辑,是理解这段代码的关键。
protected final boolean tryRelease(int releases) { // 1. 计算释放后的同步状态 int c = getState() - releases; // 2. 检查当前线程是否是锁的持有者 if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; // 3. 只有当状态值减到0时,才真正释放锁 if (c == 0) { free = true; setExclusiveOwnerThread(null); } // 4. 更新同步状态 setState(c); // 5. 返回是否完全释放 return free; }只有当状态值c减到 0 时,才会将锁的拥有者设为null,并将free标记为true。
这意味着,对于一个被重入了 n 次的锁,前n-1次调用tryRelease都会返回false,只有第 n 次才会返回true,真正释放锁。
5.3.1 公平锁和非公平锁
"公平性与否是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是 FIFO。"
这句话精准定义了公平锁的核心原则,是理解公平与非公平锁区别的关键。
核心改写位置:第 6 步「尝试获取同步状态」
AQS 的 8 步通用流程中,前 5 步(入队、自旋等待、唤醒)和后 2 步(设置头节点 / 重新阻塞)完全不变,唯一的差异在线程被唤醒后 "尝试获取同步状态" 的判断逻辑:
| 步骤 | 公平锁(FairSync) | 非公平锁(NonfairSync) |
| 6. 尝试获取同步状态 | 调用 FairSync.tryAcquire(): 1. 先检查 hasQueuedPredecessors()(队列中是否有更早请求的线程); 2. 只有队列中无前置线程,才通过 CAS 尝试获取状态。 | 调用 NonfairSync.tryAcquire()(实际是 nonfairTryAcquire()): 1. 不检查队列,直接通过 CAS 尝试抢占同步状态;2. 抢占失败后,才走队列等待逻辑。 |
protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { // 公平锁的关键:检查队列中是否有前驱节点 if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } // 可重入逻辑(与非公平锁一致) else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }公平锁的核心实现:
公平锁的关键在于
!hasQueuedPredecessors()这个条件判断:hasQueuedPredecessors()方法会检查同步队列中是否有比当前线程更早请求锁的线程。只有当队列中没有前驱线程(即当前线程是队列第一个)时,才会尝试通过 CAS 获取锁。
这个判断保证了锁的获取顺序严格遵循请求的时间顺序,实现了公平性。
非公平锁的获取逻辑
非公平锁是ReentrantLock的默认实现,它的核心特点是允许 "插队",即新线程可以直接尝试抢占锁,而无需等待队列中的线程。
非公平锁获取流程
直接抢占:当锁可用时,新线程会直接通过
CAS尝试设置同步状态,成功则立即获取锁,无需进入队列。抢占失败入队:如果抢占失败,才会被包装成
Node节点加入队列,后续逻辑与公平锁一致。唤醒后抢占:当持有锁的线程释放时,头节点的后继线程会被唤醒。但此时如果有新线程也在尝试获取锁,新线程可以和被唤醒的线程一起竞争,这就可能导致队列中的线程 "饥饿"。
5.3.2 公平与非公平锁的实现
核心逻辑总结
构造阶段:绑定 Sync 实现:当你用
new ReentrantLock(true/false)创建锁时,本质是给ReentrantLock的sync成员变量赋值:true→sync = new FairSync()(公平锁)false/无参→sync = new NonfairSync()(非公平锁)这一步就 "定死" 了后续所有获取锁的逻辑。
调用阶段:执行对应重写的 tryAcquire:当调用
lock()最终触发tryAcquire时:如果是
FairSync,就执行它重写的tryAcquire(带hasQueuedPredecessors()检查);如果是
NonfairSync,就执行它重写的tryAcquire(调用nonfairTryAcquire,无队列检查)。
ReentrantLock的构造函数正是通过选择实例化FairSync或NonfairSync,来决定锁是公平还是非公平:ReentrantLock构造函数源码
// 默认构造:非公平锁 public ReentrantLock() { sync = new NonfairSync(); } // 带参数构造:true为公平锁,false为非公平锁 public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }ReentrantLock的公平与非公平锁,是通过继承Sync这个抽象内部类,并实现不同的tryAcquire方法来区分的。
// 抽象基类,封装了AQS的通用逻辑 abstract static class Sync extends AbstractQueuedSynchronizer { // 释放锁的逻辑(公平与非公平锁共享) protected final boolean tryRelease(int releases) { /* ... */ } } // 非公平锁实现 static final class NonfairSync extends Sync { protected final boolean tryAcquire(int acquires) { // 直接尝试CAS抢占,失败再检查可重入 return nonfairTryAcquire(acquires); } } // 公平锁实现 static final class FairSync extends Sync { protected final boolean tryAcquire(int acquires) { // 先检查队列中是否有前驱,没有才尝试CAS final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } // 可重入逻辑 else if (current == getExclusiveOwnerThread()) { /* ... */ } return false; } }关键区别
非公平锁(
NonfairSync):重写tryAcquire方法时,直接调用nonfairTryAcquire,核心是无检查直接 CAS 抢占。公平锁(
FairSync):重写tryAcquire方法时,增加了hasQueuedPredecessors()检查,确保只有队列中没有前驱线程时,才会尝试 CAS。
恭喜你学习完本节内容!✿