一、AQS介绍
在JUC并发包中,AQS为其最关键的作用,全称为abstractQueuedSynchroinzed同步器,为信号量semaphore、同步锁的基础抽象类。
其中内部主要有二大块
- state
共享资源,通过并发操作state修改改值为1,如果修改成功则表示获得锁。
- FIFO队列
该队列为CLH队列的变形队列,通过引入双向队列,采取自旋加堵塞的方式提高性能,其中核心为head节点、tail节点。
二、AQS队列
/**
* 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,head是一个引用变量,会指向AQS队列的第一个节点元素,tail也是一个引用变量,也会指向AQS最后一个节点。
因为会同时多个线程节点会竞争锁,因此每一个节点都需要cas成为队尾。
2.1 head和tail的初始化
在上述注释中,明白head和tail一开始是为空的,只有多个线程竞争锁,才会进行初始化。
/**
* 当线程进行acquire失败的时候,则说明当前锁被其他线程获得,因此该线程需要进行封装成Node节点,并且插入到AQS队列中。
**/
private Node enq(final Node node) {
for (;;) {
Node t = tail;
// 如果t也就是tail为空,则说明AQS队列没有值,则需要初始化head和tail
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
由上面代码可知,如果aqs同步锁,一开始有一个线程获得lock,则会直接执行,当第二个过来,因为state的值为1,则需要将该线程设置为node进行入队伍,这里就会初始化对应的tail和head,可知这里的head和tail都是指向该节点。
当这个时候第三个线程进来,则aqs队列则会插入第三个线程c,对应tail也会发生改变。
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
2.2 waitstatus状态值
waitStatus有如下几种状态:
- CANCELLED(1):当前节点取消获取锁。当等待超时或被中断(响应中断),会触发变更为此状态,进入该状态后节点状态不再变化;
- SIGNAL(-1):后面节点等待当前节点唤醒;
- CONDITION(-2):Condition 中使用,当前线程阻塞在 Condition,如果其他线程调用了 Condition 的 signal 方法,这个结点将从等待队列转移到同步队列队尾,等待获取同步锁;
- PROPAGATE(-3):共享模式,前置节点唤醒后面节点后,唤醒操作无条件传播下去;
- 0:中间状态,当前节点后面的节点已经唤醒,但是当前节点线程还没有执行完成。
但是对于AQS的同步锁的队列需要去除CONDITION状态,在这里我们先不考虑PROPAGATE状态;
2.2.1 CANCELLED
waitstatus > 0 的时候只有一种状态为CANCElED也就是当前节点为取消状态,如果当前节点被等待超时了或者被中断了,则会变成该状态。
- 赋值时机
ReentrantLock提供tryLock(int time,TimeUnit), lockInterrupt()方法,也就是说获得锁的时候会去设置超时时间,或者设置可中断,在节点等待过程中如果发送了超时、中断则会将该节点的waitstatus设置为1,并且返回lock方法为false,表示未获得锁。
当线程被取消时,它对应的节点会被标记为取消状态,并从同步队列中清除
2.2.2 waitstatus初始状态0
我们发现这里有一个状态为0, 0表示为节点未加入队列的状态,也就是初始状态。
- 如果该节点为tail节点,则该状态一直为0,如果该节点有后续节点,后续节点在park之前会将pre节点设置为-1也就是SIGNAL, 但是如果该节点为第一个节点,则还是会被设置为-1,因为它自己的pre节点就是本身。具体参考shouldParkAfterFailedAcquire。
2.2.3 SIGNAL状态
我们在2.2.2可知道,如果节点进行park会将前节点的值设置为signal,那么这里的signal有什么用呢?
signal的状态表示:后面节点等待当前节点唤醒;
在release的时候会去唤醒后续节点。
三、如何基于AQS实现一个可重入锁
AQS提供了如下自定义方法
- tryAcquire
- tryRelease
- tryAcquireShared
- tryReleaseShared
- isHeldExclusively : 判断是否为独占锁
在编写一个锁的话,如果是独占锁,则需要实现tryAcquire和tryRelease和isHeldExclusively。
其中编写需要实现的就是对state进行操作,也就是state是为共享变量,如果state==0则表示该aqs锁未被占用,如果state>0,则表示该aqs被锁占用。
如果要使用一个锁对象,类似ReentrantLock(可重入锁),则需要实现Lock接口,该接口提供如下方法:
则需要在锁对象中创建继承aqs的内部类,并且实现对应的tryAcqurie方法、tryRelase方法以及isHeldExclusively方法。
tryAcqurie:获得锁
tryRelease: 释放锁
isHeldExclusively: 判断当前线程是否拥有锁