基于ReentrantLock深入分析AQS原理
此篇文章基于JDK8来分析的,在JDK9及以后的版本源码实现略有不同,不过思路是一样的,只是在JDK9中推出了新的类型 VarHandle 变量句柄,替代Unsafe的大部分功能。
Java中大部分同步类(ReentrantLock、CountDownLatch、Semaphore、ReentrantReadWriteLock等)都是基于AQS实现的。AQS提供了原子式管理同步状态(state)、可以阻塞和唤醒线程、FIFO双端队列模型实现的简单框架。AQS内部队列是CLH队列的变种。本篇文章从ReentrantLock入手,分析加锁和解锁过程,在ReentrantLock的场景下,AQS的使用及源码剖析。本文只是理解同步工具类及AQS原理的入门,基于这些原理,自己可以更深入的研究其他同步类中AQS的应用。
1 ReentrantLock
1.1 ReentrantLock特性
ReentrantLock和Synchronized锁
两者的共同点:
- 都是可重入锁,同一个线程可以多次获得同一把锁。
- 都保证了可见性和互斥性。
- 都是用来协调多线程对共享对象、变量的访问控制。
两者的不同点:
- ReentrantLock需要手动的加锁和解锁,synchronized系统自动释放锁。
- ReentrantLock支持公平锁,synchronized不支持。
- ReentrantLock可以尝试获取锁,设置获取锁超时时间,获取锁过程中可响应中断,使用起来更灵活。synchronized等待获取锁的过程中不能被中断。
- ReentrantLock是API级别的,synchronized是JVM级别的。
- Lock是接口,synchronized是关键字。
- ReentrantLock可以绑定多个Condition队列,synchronized只能绑定一个。
- 底层实现不同,synchronized是同步阻塞,使用的是悲观并发策略。ReentrantLock是同步非阻塞,采用的是乐观并发策略。
- synchronized发生异常时系统可以自动释放锁,所以不会放生死锁。Lock需要手动的释放锁,如果没有主动释放锁会发生死锁现象。所以Lock需要在finally代码块中释放锁。
- Lock使用读锁可以提高多线程读操作的效率。
1.2 分析ReentrantLock与AQS关系
ReentrantLock是使用AQS的同步状态、阻塞队列来实现加锁和解锁操作的,ReentrantLock只是AQS实现的其中一个场景,JUC中好多个并发类都是基于AQS来实现的,不同的场景下AQS的同步状态值代表不同的含义,具体要看同步类是怎么实现的了。
ReentrantLock与AQS关系的其实很简单,AQS相当于一个制定标准的组织,其他任何人和机构想要使用我的功能,必须实现我制定的标准。AQS制定的标准就是,所以子类继承我AQS之后,都需要实现我定义的方法,如tryAcquire 和 tryRelease 等(他里面有很多方法,具体实现哪个子类根据自己的功能来定)。这些方法不是通过子类的对象直接来调用的,是AQS自己内部封装好自己调用的,结合阻塞队列一起使用的。所以ReentrantLock类的lock方法是调用AQS的acquire,AQS的acquire方法调用tryAcquire和加入阻塞队列等方法,加入队列的方法是AQS的核心功能,所以不用继承的子类自己实现。以ReentrantLock的非公平锁为例,看下图就能明白加锁和解锁方法调用流程了:
acquire是一个模板方法,内部调用了 tryAcquire 、addWaiter、acquireQueued 方法,其中 tryAcquire 方法是在子类中重写的,所以 tryAcquire 就是一个钩子方法,具体实现子类来决定。
下面是ReentrantLock非公平锁类的关系图:
NonfairSync是ReentrantLock的内部类,NonfairSync继承自Sync,Sync继承自AbstractQueuedSynchronizer。通过这张类的关系图,再结合上一张方法之间调用图,应该就清楚加锁和解锁的大体流程了。
到这里,ReentrantLock和AQS的关系大概介绍完了,下面要详细的介绍AQS的原理,深入分析源码。
2 AQS原理分析
2.1 AQS原理概述
从整体到细节,按照以下六步流程来剖析AQS框架:
2.1.1 AQS原理概述
2.1.2 AQS数据结构
在AQS源码之前,需要先了解AQS的底层数据结构,只要掌握了它的数据结构,才能理解源码的实现。甚至需要度很多遍源码,反复的思考,才能理解作者这么写的意图。
AQS的内部数据结构——Node,Node是双向链表,是CLH队列的变体,下面介绍一下Node节点的属性和方法:
方法属性 | 解释 |
---|---|
waitStatus | 当前节点在队列中的状态 |
prev | 前驱节点 |
next | 后继节点 |
thread | 当前节点的线程 |
nextWaiter | 指向下一个处于CONDITION状态的节点(由于本篇文章不讲述Condition Queue队列,这个指针不多介绍) |
predecessor() | 返回前驱节点,若没有抛出NullPointerException |
获取锁的两种模式:
模式 | 含义 |
---|---|
SHARED | 表示多个线程可以共享同一把锁 |
EXCLUSIVE | 表示线程以独占的方式获取锁 |
waitStatus 几个枚举值的含义:
枚举值 | 含义 |
---|---|
0 | 节点被初始化时的默认值 |
SIGNAL | -1,当前线程释放或取消锁需要唤醒后继节点 |
CANCELLED | 1,当前线程以取消或中断状态 |
CONDITION | -2,与Condition相关,该标识的结点处于等待队列中,结点的线程等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。 |
PROPAGATE | -3,与共享模式相关,在共享模式中,该状态标识结点的线程处于可运行状态。 |
2.1.3 AQS同步状态
在了解数据结构后,接下来了解一下AQS的同步状态——State。AQS中维护了一个名为state的字段,意为同步状态,是由Volatile修饰的,用于展示当前临界资源的获锁情况。
// java.util.concurrent.locks.AbstractQueuedSynchronizer
private volatile int state;
state这个变量是AQS的核心,具体的含义需要继承自AQS的同步子类自己来决定。操作state的方法如下:
方法 | 含义 |
---|---|
protected final int getState() | 获取state值 |
protected final void setState(int newState) | 设置state值 |
protected final boolean compareAndSetState(int expect, int update) | 通过CAS方式更新state值 |
这几个方法都是Final修饰的,说明子类中无法重写它们。
2.1.4 AQS加入队列
通过分析ReentrantLock的加锁过程入手,里分析AQS入队列的源码。通过下面源码分析来了解。
2.1.5 AQS移除队列
通过分析ReentrantLock的解锁过程入手,里分析AQS出队列的源码。通过下面源码分析来了解。
2.1.6 AQS中断机制
AQS中用到了协作式中断的知识,AQS不处理中断,把中断结果返回给同步器自己实现,具体源码体现在 acquireQueued 方法的 parkAndCheckInterrupt 方法中,Thread.interrupted()方法检查中断标记(该方法返回了当前线程的中断状态,并将当前线程的中断标识设置为False),中断后线程也或继续获取锁,只是记录中断结果。
2.2 从ReentrantLock到AQS源码分析
下面分析ReentrantLock非公平锁加锁流程:
分析源码必须要跑起来,然后打断点一步一步执行。通过加锁和解锁入手分析
public class Lock_01 {
public static void main(String[] args) {
Lock lock = new ReentrantLock();// 默认是非公平锁,ReentrantLock(true) 公平锁,ReentrantLock(false) 非公平锁
lock.lock();
lock.unlock();
}
}
// java.util.concurrent.locks.ReentrantLock
public void lock() {
sync.lock();// 调用NonfairSync的lock方法
}
// java.util.concurrent.locks.ReentrantLock#NonfairSync
final void lock() {
if (compareAndSetState(0, 1))// CAS设置AQS的state状态值为1
setExclusiveOwnerThread(Thread.currentThread());// state设置成功,保存获取锁的线程
else
acquire(1);// 调用AQS的acquire方法
}
由于是非公平锁加锁,所以直接通过CAS修改state值,若抢到锁了,就不用进入等待队列中。这就是为什么推荐使用非公平锁,因为效率高。那为什么效率高呢?
答:进入队列需要阻塞和唤起线程,涉及到线程的上线文切换,导致系统上下文切换。在这过程中所消耗的时间大于持有锁的时间,所以线程进入队列浪费资源。另外一点,通过CAS的方式把节点加入对尾,当并发量比较大,会一直在自旋,浪费CPU资源。
上来直接 compareAndSetState(0, 1) ,设计的非常巧妙,虽然损失了代码整洁度,但是提高了性能。即使没有if这段代码,直接调用 acquire 方法也不会有问题,acquire 方法中有重复的代码。下面 addWaiter 方法中还有类似的设计。
// java.util.concurrent.locks.AbstractOwnableSynchronizer
private transient Thread exclusiveOwnerThread;
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
// java.util.concurrent.locks.AbstractQueuedSynchronizer
public final void acquire(int arg) {
if (!tryAcquire(arg) && // 尝试获取锁,tryAcquire方法调用的是子类的方法,调用NonfairSync.tryAcquire(arg),这个方法就相当于钩子方法,父类定义方法让子类自己实现
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
// AQS中tryAcquire方法实现
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
- tryAcquire方法:调用NonfairSync的tryAcquire方法,尝试获取锁,锁获取成功后,不在执行后续的加入队列的方法。
- addWaiter方法:把当前线程以安全的方式加入队列尾部。
- acquireQueued方法,尝试获取锁,获取失败阻塞当前线程。
以上这个三个方法 tryAcquire、addWaiter、acquireQueued 是AQS核心方法。tryAcquire 获取锁的核心方法,具体实现由同步器自己实现,所以自定义同步锁,加锁只需要实现tryAcquire方法就可以了。addWaiter 和 acquireQueued 是AQS的核心方法,对外透明,自定义同步锁可以不用关心这两个方法。
下面针对这三个方法(tryAcquire、addWaiter、acquireQueued)进行详细分析。
分析tryAcquire方法,AQS中acquire方法中调用的tryAcquire方法是NonfairSync的tryAcquire:
// java.util.concurrent.locks.ReentrantLock#Sync#NonfairSync
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);// 调用Sync类的nonfairTryAcquire方法
}
// java.util.concurrent.locks.ReentrantLock#Sync
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();// 获取当前线程对象
int c = getState();// 获取AQS中state状态值
if (c == 0) {// c==0,说明当前没有线程获取锁
if (compareAndSetState(0, acquires)) {// 通过CAS尝试修改state值
setExclusiveOwnerThread(current);// state修改成功,保存当前线程为独占锁线程
return true;// 返回true,获取锁成功
}
}
else if (current == getExclusiveOwnerThread()) {// 判断是否重入锁,当前线程是否是独占锁线程
int nextc = c + acquires; // 重入锁,获取锁次数加1
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");// 锁重入次数超过了int的范围,应为state的类型时int
setState(nextc);// 设置state值,保存加锁次数
return true;// 返回true,获取锁成功
}
return false;// 返回false,获取锁失败
}
// java.util.concurrent.locks.AbstractQueuedSynchronizer
protected final void setState(int newState) {
state = newState;
}
tryAcquire 方法分析完了,接着分析 addWaiter 方法。tryAcquire 获取锁失败把当前线程创建Node节点,加入AQS队列中。
// java.util.concurrent.locks.AbstractQueuedSynchronizer
public final void acquire(int arg) {
if (!tryAcquire(arg) && // tryAcquire 返回false,执行后续方法
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
下面分析 addWaiter 方法:
// java.util.concurrent.locks.AbstractQueuedSynchronizer
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);// 使用当前线程创建队列节点,node为独占模式
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;// pred 执行当前队尾
if (pred != null) { // 队列不为空
node.prev = pred; // 当前节点的前驱节点指向tail
if (compareAndSetTail(pred, node)) {// 通过CAS把当前节点设置为tail
pred.next = node;// tail的后继节点指向当前节点
return node;// 当前节点加入队尾成功,返回当前节点
}
}
enq(node);// 节点加入队列
return node;// 加入成功,返回当前节点
}
这个方法的设计与上面的lock方法思想一样,损失代码整洁度,提高性能。if代码可以没有,也没有问题,enq中会有重复的代码。
// java.util.concurrent.locks.AbstractQueuedSynchronizer
private Node enq(final Node node) {
for (;;) {// 死循环,通过自旋的方式,直道成功的把节点加入队尾
Node t = tail;// t保存当前队尾指针
if (t == null) { // Must initialize 队列为空
if (compareAndSetHead(new Node())) // 通过CAS初始化head
tail = head;// tail指向head节点
} else {
node.prev = t;// 当前节点的next指向尾节点
if (compareAndSetTail(t, node)) {// 通过CAS把当前节点设置为尾结点
t.next = node;// 原尾结点的next指向当前节点
return t; 返回原尾结点
}
}
}
}
当t==null时,创建了一个空的节点作为头结点,头结点是空节点不保存任何线程,因为唤醒节点线程继续执行需要头节点。
到此,把当前线程成功加入队列中。接着分析 acquireQueued 方法:
// java.util.concurrent.locks.AbstractQueuedSynchronizer
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;// 存储线程中断状态
for (;;) {
final Node p = node.predecessor();// 获取node节点的前驱节点
if (p == head && tryAcquire(arg)) {// 如果当前节点是头结点的后继节点,可以尝试获取锁,应为有可能头结点已经释放锁,但是还没来得及通知后继节点,所以可以尝试去获取锁
setHead(node);// 获取锁成功,设置当前节点为头节点
p.next = null; // help GC
failed = false;
return interrupted;// 返回线程中断状态
}
if (shouldParkAfterFailedAcquire(p, node) && // 找当前节点之前的有效(未取消)节点,当前节点next指向该节点,设置前驱节点的waitState=-1,用于唤醒当前接口
parkAndCheckInterrupt())// 阻塞线程,返回中断状态
interrupted = true;// 线程在阻塞的过程中中断过
}
} finally {
if (failed) // 如果线程抛出异常,没有正常执行到这里,failed的值为true,所以会取消当前节点
cancelAcquire(node);
}
}
这个方法主要功能如下:
- 当前节点尝试获取锁,获取成功,直接返回。获取失败,执行一下步骤;
- 找到当前节点的有效前驱节点,并把新找到的前驱节点的waitState值改为-1;
- 阻塞当前线程,等待前驱节点唤醒。
- 若以上步骤执行的过程中线程抛出异常(中断等),取消当前节点,若当前节点为头结点,唤醒后继节点。
p==head,为什么需要尝试获取锁呢?
答:这个需要结合unlock一起分析了,这里先说一下结果,unlock释放锁后(就是把state改成0),没有来得及通知后继节点,这时当前节点尝试去获取锁,若获取成功后就不用阻塞线程,提高性能,降低阻塞和唤醒的资源浪费,因为涉及到上下文切换。
分析一下 shouldParkAfterFailedAcquire 方法:
// java.util.concurrent.locks.AbstractQueuedSynchronizer
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* 前驱节点的waitStatus=-1,当前节点可以安全的被阻塞
*/
return true;
if (ws > 0) {
/*
* 如果前驱节点被取消,跳过前驱节点,继续向前找
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;// 找到未取消的前驱节点的next指向当前节点
} else {
/*
* 通过CAS把当前节点的前驱节点waitStatus值改为-1
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
这一步只要是找到前驱节点,并把前驱节点waitStatus值改为-1,确保当前节点阻塞后,可被唤醒。
分析 parkAndCheckInterrupt 方法:
// java.util.concurrent.locks.AbstractQueuedSynchronizer
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);// 阻塞当前线程
return Thread.interrupted();// 获取线程阻塞的过程中是否被中断,若中断return返回true,并将中断状态改为false。AQS不处理中断操作,具体如何处理由实现类决定。
}
分析 cancelAcquire 方法:
// java.util.concurrent.locks.AbstractQueuedSynchronizer
private void cancelAcquire(Node node) {
if (node == null)
return;
node.thread = null;
// 跳过已经取消的前驱节点
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// node前驱节点的next指针
Node predNext = pred.next;
// 修改node节点的waitStatus值为-1,已取消状态
node.waitStatus = Node.CANCELLED;
// 如果node是尾结点,通过CAS把node前驱节点设置为尾结点
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);// 通过CAS把前驱节点的next指向null
} else {
int ws;
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {// 前驱节点不是头结点,并且前驱节点的waitStatus是-1或者可以改成-1,并且前驱节点线程不为空
Node next = node.next;
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);// 修改前驱节点的next指针
} else {
unparkSuccessor(node);// 前驱节点是头结点,或者前驱节点已经取消了,唤醒当前节点的后继节点继续执行(但是不一定获取锁,有可能继续被阻塞)
}
node.next = node; // help GC
}
}
分析 unparkSuccessor 方法:这个方法比较简单,从后往前找,找到离当前节点最近的后继节点,然后唤醒,让其继续执行。
private void unparkSuccessor(Node node) {
// 尝试清除当前节点的等待状态,如果此操作失败或等待的线程更改了状态,也没有问题。
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 先判断当前节点的后继节点是否有效(不为空并且没取消),有效直接唤醒
Node s = node.next;
if (s == null || s.waitStatus > 0) {// 若当前节点的后继节点无效或取消,从后往前找离当前节点最近的未取消节点
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);// 唤醒节点线程
}
思考为什么需要从后往前找,而不是从前往后找?
答:前面已经分析节点加入队列的步骤了,先设置当前节点的前驱节点,node.prev = t,再通过CAS设置队列尾结点tail,最后设置prev.next=node。这三步不是原子操作,在尾结点设置成功之后,node.prev=t 一定成功了,但是 prev.next=node 操作并不一定执行,所以从后往前找是没问题的,但是从前往后找就会导致缺失节点。
到这里,非公平锁加锁流程就分析完了,简单总结一下:尝试获取锁,得到锁,成功返回。未得到锁,加入队列,阻塞线程。
非公平锁解锁流程:
public class Lock_01 {
public static void main(String[] args) {
Lock lock = new ReentrantLock();// 默认是非公平锁,ReentrantLock(true) 公平锁,ReentrantLock(false) 非公平锁
lock.lock();
lock.unlock();
}
}
// java.util.concurrent.locks.ReentrantLock
public void unlock() {
sync.release(1);// 调用父类AQS的release方法
}
分析 release 方法:
// java.util.concurrent.locks.AbstractQueuedSynchronizer
public final boolean release(int arg) {
if (tryRelease(arg)) {// 尝试释放锁,tryRelease这个方法调用子类的实现
Node h = head;// h指向当前头结点
if (h != null && h.waitStatus != 0)// 判断是否有需要唤醒的后继节点
unparkSuccessor(h);// 唤醒头结点的后继节点
return true;// 返回true,解锁成功
}
return false;// 返回false,解锁失败
}
分析 tryRelease 方法:
// java.util.concurrent.locks.ReentrantLock#Sync
protected final boolean tryRelease(int releases) {
int c = getState() - releases;// 计算剩余锁重入次数
if (Thread.currentThread() != getExclusiveOwnerThread())// 判断当前线程是否是获取锁的线程,若不是直接抛出异常
throw new IllegalMonitorStateException();
boolean free = false;// 标识是否释放锁,true-释放 false-不释放
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);// 清除当前持有锁的线程
}
setState(c);// 修改加锁次数
return free;// 返回锁释放状态
}
先判断当前线程是否是独占锁持有线程,若不是直接抛出异常。若是,判断state==0,等于0表示当前线程可以释放锁,清除独占锁线程标识,state值设置为0或者剩余锁重入次数。
上面释放锁的动作不是原子性的,在执行完 setState(0) 方法后,锁已经被释放了,此时再来新的线程可以拿到这把锁,因为之前已经分析非公平锁加锁流程了,上线先通过CAS修改state值,修改成功就拿到锁了。
在最极端的情况下,非公平锁会导致已经在队列中的线程一直获取不到锁,也就是锁饥饿问题。不过这种情况发生的概率比较小,可以忽略。若实际生产中并发量特别大,需要解决这个问题,可使用公平锁,没有线程获取锁时都需要先看队列中是否有有效的节点,只要有,当前节点就必须加入队列等待被唤醒。这就可以保证先入队列优先获取到锁。
下面看一下公平锁的加锁流程,主体思路与非公平锁一样,只是尝试获取锁的方式不同,这个不做详细分析了,主要看 tryAcquire 方法:
public class Lock_01 {
public static void main(String[] args) {
Lock lock = new ReentrantLock(true);// 默认是非公平锁,ReentrantLock(true) 公平锁,ReentrantLock(false) 非公平锁
lock.lock();
lock.unlock();
}
}
// java.util.concurrent.locks.ReentrantLock
public void lock() {
sync.lock();
}
// java.util.concurrent.locks.ReentrantLock#FairSync
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 方法:
public final boolean hasQueuedPredecessors() {
Node t = tail; // t指向尾节点
Node h = head; // h指向头节点
Node s;
return h != t &&// h!=t,说明队列中有节点
((s = h.next) == null || s.thread != Thread.currentThread());
}
这个方法的核心就在于最后一句代码,h!=t,说明队列中有节点。h.next==null,什么情况下会发生这种情况呢?理解这种情况,需要考虑往队列里添加节点的步骤:
- node.prev = pred,当前节点的前驱节点指向tail
- compareAndSetTail(pred, node),当前节点设置为tail
- pred.next = node,前驱节点的next指向node
当1,2执行成功了,3还没执行,此时h.next的值就为null。若头结点的后继节点不是当前线程,就加入队列排队,若后继节点是当前线程,就尝试通过CAS修改state值来获取锁。
3 AQS应用
3.1 AQS在JUC中的应用场景
JUC中很多并发工具底层都是基于AQS实现的,下面介绍几种同步工具AQS的应用场景:
同步工具 | 同步工具与AQS的关联 |
---|---|
ReentrantLock | 使用AQS保存加锁的次数。ReentrantLock加锁成功后记录获得锁线程ID,用于判断锁重入和禁止其他线程解锁。 |
CountDownLatch | 使用AQS同步状态计数。每执行一次countDown,AQS同步状态值减1,减到0时,所有阻塞线程被唤醒,可以继续执行。 |
Semaphore | 使用AQS同步状态保存信号量当前数量。每执行一次acquire,信号量当前数量减1,当减到0时,线程进入AQS阻塞队列,等待其他线程执行release后,才能唤醒阻塞队列头上的线程。 |
ReentrantReadWriteLock | AQS同步状态state是int类型的,占用4个字节32位,低16位存写锁的数量,高16位存读锁的数量。 |
3.2 自定义同步工具
实现自定义同步工具很简单,只需要继承AbstractQueuedSynchronizor,重写 tryAcquire 和 tryRelease 方法,这两个方法是加锁和解锁的核心实现方法,再提供两个Api接口 lock 和 unlock 用于加锁和解锁,这两个方法内部直接通过自定义同步工具对象调用AQS的 acquire 和 release 方法。
// java.util.concurrent.locks.AbstractQueuedSynchronizer
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
通过源码可以看到AQS内部并没有实现tryAcquire方法,这个方法需要自定义同步器自己来实现。AQS中acquire方法调用的是子类的tryAcquire,所以tryAcquire是钩子方法,需要继承AQS的子类实现,acquire方法采用模板方法设计模式。
// java.util.concurrent.locks.AbstractQueuedSynchronizer
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
tryRelease方法与上面的tryAcquire原理相同。
根据上面的分析,实现简易版的自定义同步器 MyLock:
public class MyLock {
private final Sync sync = new Sync();
public void lock() {
sync.acquire(1);
}
public void unlock() {
sync.release(0);
}
private static class Sync extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire(int arg) {
return compareAndSetState(0, arg);
}
@Override
protected boolean tryRelease(int arg) {
setState(arg);
return true;
}
}
}
测试自己定义同步工具MyLock:
public class MyLockTest {
static int count = 0;
public static void main(String[] args) throws InterruptedException {
MyLock lock = new MyLock();
Runnable r = () -> {
try {
lock.lock();
for (int i = 0; i < 1000000; i++) {
count++;
}
} finally {
lock.unlock();
}
};
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
不管运营多少遍,最终输出结果都是:2000000
参考资料: