AQS、ReentrantLock
约 6684 字大约 22 分钟
2025-01-22
AQS
简介
AQS,列同步器 AbstractQueuedSynchronizer(后面简称 AQS)是实现锁和有关同步器的一个基础框架。
在 JDK5 中,并发包中加入了大量的同步工具,例如重入锁(ReentrantLock)、读写锁(ReentrantReadWriteLock)、信号量(Semaphore)、CountDownLatch(倒计时锁)等,都是基于 AQS 的。其内部通过一个被标识为 volatile 的名为 state 的变量来控制多个线程之间的同步状态。多个线程之间可以通过 AQS 来独占式或共享式的抢占资源。
基于 AQS,可以很方便的实现 Java 中不具备的功能。
例如,在锁这个问题上,Java 中提供的是synchronized关键字,用这个关键字可以很方便的实现多个线程之间的同步。但这个关键字也有很多缺陷,比如:
- 不支持超时的获取锁
- 不可响应中断
- 不能尝试获取锁
而 ReentrantLock 基于 AQS 将上述几点都做到了
| synchronized | AQS | |
|---|---|---|
| 实现方式 | 关键字,C++实现 | Java 实现 |
| 锁类型,锁释放 | 悲观锁,自动释放锁 | 悲观锁,手动开启和关闭 |
| 锁竞争 | 激烈,性能差 | 提供多种解决方案 |
工作机制
- AQS 内部维护一个先进先出的双向队列,存储排队获取锁的线程
- AQS 内部有一个属性 state,相当于是一个资源, 默认为 0;若队列中有一个线程将 state 修改了为 1,则意味着当前线程获取了资源
- 原子性:对 state 修改时,使用 cas 操作,保证了 多个线程修改的原子性
核心结构
从 AbstractQueuedSynchronizer 的名字可以看出,AQS 中一定是基于队列实现的(Queue)。
在 AQS 内部,是通过链表实现的队列。
链表的每个元素是其内部类 Node 的一个实现。然后 AQS 通过实例变量 head 指向队列的头,通过实例变量 tail 指向队列的尾。

其源码定义如下:
/**
* Head of the wait queue, lazily initialized. Except for
* initialization, it is modified only via method setHead. Note:
* If head exists, its waitStatus is guaranteed not to be
* CANCELLED.
*/
private transient volatile Node head;
/**
* Tail of the wait queue, lazily initialized. Modified only via
* method enq to add new wait node.
*/
private transient volatile Node tail;
/**
* The synchronization state.
*/
private volatile int state;
static final class Node {
/** 标识为共享式 */
static final Node SHARED = new Node();
/** 标识为独占式 */
static final Node EXCLUSIVE = null;
/** 同步队列中等待的线程等待超时或被中断,需要从等待队列中取消等待,进入该状态的节点状态将不再变化 */
static final int CANCELLED = 1;
/** 当前节点的后继节点处于等待状态,且当前节点释放了同步状态,需要通过unpark唤醒后继节点,让其继续运行 */
static final int SIGNAL = -1;
/** 当前节点等待在某一Condition上,当其他线程调用这个Conditino的signal方法后,该节点将从等待队列恢复到同步队列中,使其有机会获取同步状态 */
static final int CONDITION = -2;
/** 表示下一次共享式同步状态获取状态将无条件的传播下去 */
static final int PROPAGATE = -3;
/* 当前节点的等待状态,取值为上述几个常量之一,另外,值为0表示初始状态 */
volatile int waitStatus;
/* 前驱节点 */
volatile Node prev;
/* 后继节点 */
volatile Node next;
/* 等待获取同步状态的线程 */
volatile Thread thread;
/* 等待队列中的后继节点 */
Node nextWaiter;
// ...
}设计模型

组成部分
AQS 主要由三部分组成
- state 同步状态
- Node 组成的 CLH 队列
- ConditionObject 条件变量(包含 Node 组成的条件单向队列)。
state 用 volatile 来修饰,保证了我们操作的可见性,所以任何线程通过 getState() 获得状态都是可以得到最新值,但是 setState() 无法保证原子性,因此 AQS 给我们提供了 compareAndSetState 方法利用底层 UnSafe 的 CAS 功能来实现原子性。
State 关键字
对于 AQS 来说,线程同步的关键是对 state 的操作,可以说获取、释放资源是否成功都是由 state 决定的,比如 state>0 代表可获取资源,否则无法获取,所以 state 的具体语义由实现者去定义,现有的 ReentrantLock、ReentrantReadWriteLock、Semaphore、CountDownLatch 定义的 state 语义都不一样。
- ReentrantLock 的 state 用来表示是否有锁资源,变量记录了锁的重入次数
- ReentrantReadWriteLock 的 state 高 16 位代表读锁状态,低 16 位代表写锁状态
- Semaphore 的 state 用来表示可用信号的个数
- CountDownLatch 的 state 用来表示计数器的值
实现的两类队列
- 同步队列:服务于线程阻塞等待获取资源
- 条件队列:服务于线程因某个条件不满足而进入等待状态。 条件队列中的线程实际上已经获取到了资源,但是没有能够继续执行下去的条件,所以被打入条件队列并释放持有的资源,以让渡其它线程执行,如果未来某个时刻条件得以满足,则该线程会被从条件队列转移到同步队列,继续参与竞争资源,以继续向下执行。
同步队列
CLH
同步队列是基于链表实现的双向队列,也是 CLH 锁的变种。CLH 锁是 AQS 队列同步器实现的基础。
以下图为CLH 的构成:

- CLH 锁是有由 Craig, Landin, and Hagersten 这三个人发明的锁,取了三个人名字的首字母,所以叫 CLH Lock。
- CLH 锁是一个自旋锁。能确保无饥饿性。提供先来先服务的公平性。
- CLH 队列锁也是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程仅仅在本地变量上自旋,它不断轮询前驱的状态,假设发现前驱释放了锁就结束自旋。
Node
AQS 以内部类 Node 的形式定义了同步队列结点。这就是前文看到的第一个内部类。
static final class Node {
/** 模式定义 */
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
/** 线程状态 */
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
/** 线程等待状态 */
volatile int waitStatus;
/** 前驱结点 */
volatile Node prev;
/** 后置结点 */
volatile Node next;
/** 持有的线程对象 */
volatile Thread thread;
/** 对于独占模式而言,指向下一个处于 CONDITION 等待状态的结点;对于共享模式而言,则为 SHARED 结点 */
Node nextWaiter;
// ... 省略方法定义
}Node 在 CLH 的基础上进行了变种。
CLH 是单向队列,其主要特点是自旋检查前驱节点的 locked 状态。
而 AQS 同步队列是 双向队列,每个节点也有状态 waitStatus,而其并不是一直对前驱节点的状态自旋判断,而是自旋一段时间后阻塞让出 cpu 时间片(上下文切换),等待前驱节点主动唤醒后继节点。
waitStatus 有如下 5 中状态:
- CANCELLED = 1 表示当前结点已取消调度。当超时或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。
- SIGNAL = -1 表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为 SIGNAL。
- CONDITION = -2 表示结点等待在 Condition 上,当其他线程调用了 Condition 的 signal() 方法后,CONDITION 状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
- PROPAGATE = -3 共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。
- INITIAL = 0 新结点入队时的默认状态。
从上面的代码中可以看出,位于 CLH 链表中的线程以 2 种模式在等待资源,即 SHARED 和 EXCLUSIVE,其中 SHARED 表示共享模式,而 EXCLUSIVE 表示独占模式。
共享模式与独占模式的主要区别在于,同一时刻独占模式只能有一个线程获取到资源,而共享模式在同一时刻可以有多个线程获取到资源。典型的场景就是读写锁,读操作可以有多个线程同时获取到读锁资源,而写操作同一时刻只能有一个线程获取到写锁资源,其它线程在尝试获取资源时都会被阻塞。
主要行为
AQS 类成员变量 head 和 tail 字段分别指向同步队列的头结点和尾结点:
/**
* Head of the wait queue, lazily initialized. Except for
* initialization, it is modified only via method setHead. Note:
* If head exists, its waitStatus is guaranteed not to be
* CANCELLED.
*/
private transient volatile Node head;
/**
* Tail of the wait queue, lazily initialized. Modified only via
* method enq to add new wait node.
*/
private transient volatile Node tail;其中 head 表示同步队列的头结点,而 tail 则表示同步队列的尾结点,具体组织形式如下图:

当调用 AQS 的 acquire 方法获取资源时,如果资源不足则当前线程会被封装成 Node 结点添加到同步队列的末端(入队),头结点 head 用于记录当前正在持有资源的线程结点,而 head 的后继结点就是下一个将要被调度的线程结点,当 release 方法被调用时,该结点上的线程将被唤醒(出队),继续获取资源。
同步队列的主要行为是 :入队、出队
- 入队
获取资源失败的线程需要封装成 Node 节点,接着尾部入队,在 AQS 中提供 addWaiter 函数完成 Node 节点的创建与入队。添加节点的时候,如 CLH 队列已经存在,通过 CAS 快速将当前节点添加到队列尾部,如果添加失败或队列不存在,则初始化同步队列。
/**
* Creates and enqueues node for current thread and given mode.
*
* @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
* @return the new node
*/
private Node addWaiter(Node mode) {
Node node = new Node(mode);
for (;;) {
Node oldTail = tail;
if (oldTail != null) {
node.setPrevRelaxed(oldTail);
if (compareAndSetTail(oldTail, node)) {
oldTail.next = node;
return node;
}
} else {
initializeSyncQueue();
}
}
}总结:线程获取锁失败,入队列,将新节点加到 tail 后面,然后对 tail 进行 CAS 操作,将 tail 指针后移到新节点上。
- 出队
CLH 队列中的节点都是获取资源失败的线程节点,当持有资源的线程释放资源时,会将 head.next 指向的线程节点唤醒(CLH 队列的第二个节点),如果唤醒的线程节点获取资源成功,线程节点清空信息设置为头部节点(新哨兵节点),原头部节点出队(原哨兵节点)
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) { // 如果 state=0 了,就是可以释放锁了
free = true;
setExclusiveOwnerThread(null); // 将拿锁线程置为 null
}
setState(c); // 重置同步器的 state
return free; // 返回是否成功释放
}
private void unparkSuccessor(Node node) {
// node 节点是当前释放锁的节点,也是同步队列的头节点
int ws = node.waitStatus;
// 如果节点已经被取消了,把节点的状态置为初始化
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 拿出队二 s
Node s = node.next;
// s 为空,表示 node 的后一个节点为空
// s.waitStatus 大于 0,代表 s 节点已经被取消了
// 遇到以上这两种情况,就从队尾开始,向前遍历,找到第一个 waitStatus 字段不是被取消的
if (s == null || s.waitStatus > 0) {
s = null;
// 结束条件是前置节点就是 head 了
for (Node t = tail; t != null && t != node; t = t.prev)
// t.waitStatus <= 0 说明 t 当前没有被取消,肯定还在等待被唤醒
if (t.waitStatus <= 0)
s = t;
}
// 唤醒以上代码找到的线程
if (s != null)
LockSupport.unpark(s.thread);
}总结:出队列,锁释放唤醒 head 的后继节点,head 的后继节点从阻塞中醒来,开始抢锁,获取锁成功,此时 head 指针向后移一个位置,原先 head 的后继节点成为新的 head。
条件队列
一个 AQS 可以对应多个条件变量

ConditionObject 内部维护着一个单向条件队列,不同于 CLH 队列,条件队列只入队执行 await 的线程节点,并且加入条件队列的节点,不能在 CLH 队列, 条件队列出队的节点,会入队到 CLH 队列。
当某个线程执行了 ConditionObject 的 await 函数,阻塞当前线程,线程会被封装成 Node 节点添加到条件队列的末端,其他线程执行 ConditionObject 的 signal 函数,会将条件队列头部线程节点转移到 CLH 队列参与竞争资源,具体流程如下图:

一个 Condition 对象就有一个单项的等待任务队列。在一个多线程任务中我们可以 new 出多个等待任务队列。比如我们 new 出来两个等待队列。
private Lock lock = new ReentrantLock();
private Condition FirstCond = lock.newCondition();
private Condition SecondCond = lock.newCondition();所以真正的 AQS 任务中一般是一个任务队列 N 个等待队列的,因此我们尽量调用 signal 而少用 signalAll,因为在指定的实例化等待队列中只有一个可以拿到锁的。
总结
- 状态管理:AQS 内部维护了一个状态变量 state,通过该状态变量来表示共享资源的状态,可以是独占模式也可以是共享模式。
- CAS 操作:AQS 使用 CAS(Compare And Swap)操作来实现对状态变量的原子性修改,确保线程安全性。
- 线程阻塞和唤醒:当一个线程尝试获取锁或访问资源时,如果资源已被其他线程占用,则会将该线程阻塞并加入同步等待队列,直到资源可用时再唤醒线程。
- 双向链表:AQS 使用双向链表来管理等待线程,保持线程之间的先后顺序,即按照先进先出的原则进行访问。
- 模板方法设计模式:AQS 提供了模板方法,允许子类通过实现特定的方法来控制锁的获取和释放过程,从而实现不同类型的同步器。
ReentrantLock
ReentrantLock,重入锁,是 JDK5 中添加在并发包下的一个高性能的工具。
顾名思义,ReentrantLock 支持同一个线程在未释放锁的情况下重复获取锁。
synchronized 和 ReentrantLock
性能上的比较
首先,ReentrantLock 的性能要优于 synchronized。下面通过两段代码比价一下。 首先是 synchronized:
PS:当存在大量线程竞争锁时,多数情况下 ReentrantLock 的性能优于 synchronized。
因为在JDK6中对 synchronized 做了优化
在锁竞争不激烈的时候,多数情况下锁会停留在偏向锁和轻量级锁阶段,这两个阶段性能是很好的。
当存在大量竞争时,可能会膨胀为重量级锁,性能下降,此时的 ReentrantLock 应该是优于 synchronized 的。
获取公平锁
公平性是啥概念呢?如果是公平的获取锁,就是说多个线程之间获取锁的时候要排队,依次获取锁;如果是不公平的获取锁,就是说多个线程获取锁的时候一哄而上,谁抢到是谁的。
由于synchronized 是基于 monitor 机制实现的,它只支持非公平锁;
但 ReentrantLock 同时支持公平锁和非公平锁。
ReentrantLock 还有一些其他 synchronized 不具备的特性
| synchronized | ReentrantLock | |
|---|---|---|
| 性能 | 相对较差 | 优于 synchronized 20%左右 |
| 公平性 | 非公平锁 | 公平锁、非公平锁 |
| 尝试获取锁 | 不支持,没获取到锁就阻塞 | 支持,通过 tryLock 方法实现 |
| 超时获取锁 | 不支持,没获取到锁就阻塞 | 支持,通过 tryLock(time, TimeUnit) 方法实现 |
| 是否可响应中断 | 不支持,不可响应线程的 interrupt 信号 | 支持,通过 lockInterruptibly() 方法实现 |
| 等待条件的支持 | 支持,通过 wait 、notify 、notifyAll 来实现 | 支持,通过 Condition 接口实现 |
可重入实现原理
ReentrantLock 的实现基于队列同步器(AbstractQueuedSynchronizer)后面简称 AQS
ReentrantLock 的可重入功能基于 AQS 的同步状态:state。
核心原理:当某一线程获取锁后,将 state 值+1,并记录下当前持有锁的线程,
再有线程来获取锁时,判断这个线程与持有锁的线程是否是同一个线程,如果是,将 state 值再+1,如果不是,阻塞线程。 当线程释放锁时,将 state 值-1
当 state 值减为 0 时,表示当前线程彻底释放了锁,然后将记录当前持有锁的线程的那个字段设置为 null,并唤醒其他线程,使其重新竞争锁。
// acquires的值是1
final boolean nonfairTryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 获取state的值
int c = getState();
// 如果state的值等于0,表示当前没有线程持有锁
// 尝试将state的值改为1,如果修改成功,则成功获取锁,并设置当前线程为持有锁的线程,返回true
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// state的值不等于0,表示已经有其他线程持有锁
// 判断当前线程是否等于持有锁的线程,如果等于,将state的值+1,并设置到state上,获取锁成功,返回true
// 如果不是当前线程,获取锁失败,返回false
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}非公平锁实现原理
ReentrantLock 有两个构造函数:
// 无参构造,默认使用非公平锁(NonfairSync)
public ReentrantLock() {
sync = new NonfairSync();
}
// 通过fair参数指定使用公平锁(FairSync)还是非公平锁(NonfairSync)
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}sync 是 ReentrantLock 的成员变量,是其内部类 Sync 的实例。NonfairSync 和 FairSync 都是 Sync 类的子类。可以参考如下类关系图:

Sync 继承了 AQS,所以他具备了 AQS 的功能。同样的,NonfairSync 和 FairSync 都是 AQS 的子类。
当我们通过无参构造函数获取 ReentrantLock 实例后,默认用的就是非公平锁。
加锁
下面将通过如下场景描述非公平锁的实现原理:假设一个线程(t1)获取到了锁,其他很多没获取到锁的线程(others_t)加入到了 AQS 的同步队列中等待,当这个线程执行完,释放锁后,其他线程重新非公平的竞争锁。
注意:新来的线程执行 lock 方法时,都要尝试下能不能直接获取锁,若获取锁成功,则记录当前线程,否则调用 AQS 的 acqurire 方法,进入到同步队列中等待
final void lock() {
// 线程t1成功的将state的值从0改为1,表示获取锁成功
// 并记录当前持有锁的线程
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
// others_t线程们没有获取到锁
acquire(1);
}如果获取锁失败,会调用 AQS 的 acquire 方法:
public final void acquire(int arg) {
// tryAcquire是个模板方法,在NonfairSync中实现,如果在tryAcquire方法中依然获取锁失败,会将当前线程加入同步队列中等待(addWaiter)
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}tryAcquire 的实现如下,其实是调用了上面的 nonfairTryAcquire 方法
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}释放锁
OK,此时 t1 获取到了锁,others_t 线程们都跑到同步队列里等着了。
某一时刻,t1 自己的任务执行完成,调用了释放锁的方法(unlock)。
public void unlock() {
// 调用AQS的release方法释放资源
sync.release(1);
}
public final boolean release(int arg) {
// tryRelease也是模板方法,在Sync中实现
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
// 成功释放锁后,唤醒同步队列中的下一个节点,使之可以重新竞争锁
// 注意此时不会唤醒队列第一个节点之后的节点,这些节点此时还是无法竞争锁
unparkSuccessor(h);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
// 将state的值-1,如果-1之后等于0,释放锁成功
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}这时锁被释放了,被唤醒的线程(一个)和新来的线程重新竞争锁(不包含同步队列后面的那些线程)。
回到 lock 方法中,由于此时所有线程都能通过 CAS 来获取锁,并不能保证被唤醒的那个线程能竞争过新来的线程,所以是非公平的。这就是非公平锁的实现。
这个过程大概可以描述为下图这样子:

公平锁实现原理
公平锁与非公平锁的释放锁的逻辑是一样的,都是调用上述的 unlock 方法,最大区别在于获取锁的时候:直接调用的 AQS 的 acquire 方法,没有先尝试获取锁。
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
// 获取锁,与非公平锁的不同的地方在于,这里直接调用的AQS的acquire方法,没有先尝试获取锁
// acquire又调用了下面的tryAcquire方法,核心在于这个方法
final void lock() {
acquire(1);
}
/**
* 这个方法和nonfairTryAcquire方法只有一点不同,在标注为#1的地方
* 多了一个判断hasQueuedPredecessors,这个方法是判断当前AQS的同步队列中是否还有等待的线程
* 如果有,返回true,否则返回false。
* 由此可知,当队列中没有等待的线程时,当前线程才能尝试通过CAS的方式获取锁。
* 否则就让这个线程去队列后面排队。
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// #1
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;
}
}通过注释可知,在公平锁的机制下,任何线程想要获取锁,都要排队,不可能出现插队的情况。这就是公平锁的实现原理。
这个过程大概可以描述为下图这样子:

tryLock 原理
tryLock 做的事情很简单:让当前线程尝试获取一次锁,成功的话返回 true,否则 false。
其实就是调用了nonfairTryAcquire方法来获取锁。
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}至于获取失败的话,他也不会将自己添加到同步队列中等待,直接返回 false,让业务调用代码自己处理。
可中断的获取锁
中断,也就是通过 Thread 的 interrupt 方法将某个线程中断,中断一个阻塞状态的线程,会抛出一个 InterruptedException 异常。
如果获取锁是可中断的,当一个线程长时间获取不到锁时,我们可以主动将其中断,可避免死锁的产生。
其实现方式如下:
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}会调用 AQS 的 acquireInterruptibly 方法:
public final void acquireInterruptibly(int arg)
throws InterruptedException {
// 判断当前线程是否已经中断,如果已中断,抛出InterruptedException异常
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}此时会优先通过 tryAcquire 尝试获取锁,如果获取失败,会将自己加入到队列中等待,并可随时响应中断。
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
// 将自己添加到队列中等待
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
// 自旋的获取锁
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
// 获取锁失败,在parkAndCheckInterrupt方法中,通过LockSupport.park()阻塞当前线程,
// 并调用Thread.interrupted()判断当前线程是否已经被中断
// 如果被中断,直接抛出InterruptedException异常,退出锁的竞争队列
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
// #1
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}PS:不可中断的方式下,代码#1 位置不会抛出 InterruptedException 异常,只是简单的记录一下当前线程被中断了。
可超时的获取锁
通过如下方法实现,timeout 是超时时间,unit 代表时间的单位(毫秒、秒...)
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}可以发现,这也是一个可以响应中断的方法。然后调用 AQS 的 tryAcquireNanos 方法:
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}doAcquireNanos 方法与中断里面的方法大同小异,下面在注释中说明一下不同的地方:
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
// 计算超时截止时间
final long deadline = System.nanoTime() + nanosTimeout;
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
// 计算到截止时间的剩余时间
nanosTimeout = deadline - System.nanoTime();
if (nanosTimeout <= 0L) // 超时了,获取失败
return false;
// 超时时间大于1000纳秒时,才阻塞
// 因为如果小于1000纳秒,基本可以认为超时了(系统调用的时间可能都比这个长)
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
// 响应中断
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}小结
本文首先对比了元老级的锁 synchronized 与 ReentrantLock 的不同,ReentrantLock 具有一下优势: 同时支持公平锁与非公平锁 支持:尝试非阻塞的一次性获取锁 支持超时获取锁 支持可中断的获取锁 * 支持更多的等待条件(Condition)
然后介绍了几个主要特性的实现原理,这些都是基于 AQS 的。
- 首先,ReentrantLock 采用了独占模式,即同一时刻只允许一个线程持有锁。这保证了被锁保护的临界区只能被一个线程访问,从而避免了多个线程同时修改共享资源导致的数据竞争和不一致性。
- ReentrantLock 的核心,是通过修改 AQS 中 state 的值来同步锁的状态。 通过这个方式,实现了可重入。
- ReentrantLock 具备公平锁和非公平锁,默认使用非公平锁。其实现原理主要依赖于 AQS 中的同步队列。
最后,可中断的机制是内部通过 Thread.interrupted()判断当前线程是否已被中断,如果被中断就抛出 InterruptedException 异常来实现的。
版权所有
版权归属:haipeng-lin