AQS(AbstractQueuedSynchronizer)
AQS
: 是一个用来构建锁和同步器的框架,使用AQS
能简单且高效的构造出应用广泛的大量同步器,比如(ReentrantLock
,Semaphore
)
AQS
是一个FIFO
的双向队列,内部通过节点head
和tail
记录队首和队尾元素,队列元素的类型为Node
;
state
:单一的状态信息;
对于
ReentrantLock
而言,state
表示当前线程获取锁的可重入次数。(当一个线程获取到ReentrantLock
锁后,在AQS
内部首先使用CAS
操作把state
状态值从0变为1
,然后设置当前锁的持有者为当前线程,当该线程再次获取锁时发现它就是锁的持有者,则会改变state
的值由1变为2
)对于
ReentrantReadWriteLock
而言,state
的高16位
表示读状态(读锁的次数),低16位
表示获取到写锁的线程的可重入次数对于
Semaphore
而言,state
表示当前可用信号的个数对于
CountDownlatch
而言,state
表示计数器当前的值
对于AQS
来说,线程同步的关键是对状态值state
进行操作。根据state
是否属于一个线程,操作state
的方式分为独占方式和共享方式。
独占式
独占方式下,获取和释放资源的流程如下。
只有一个线程能执行,如 ReentrantLock
。又可分为公平锁和非公平锁
/**
* 首先使用tryAcquire方法尝试获取资源,具体是设置状态变量state的值,
* 成功则直接返回,失败则将当前线程封装为类型为
* Node.EXCLUSIVE的Node节点通过addWaiter()方法插入到AQS阻塞队列的尾部,
* 最后调用acquireQueued(Node node,int arg)方法,使得该节点以死循环的方式
* 获取同步状态,如果获取不到则调用LockSupport.park(this)方法挂起自己,
* 被挂起的线程唤醒主要依靠前驱节点的出队或阻塞线程被中断来实现
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
/**
* 首先尝试使用tryRelease操作释放资源,这里是设置状态变量state的值。
* 然后调用 LockSupport.unpark(thread)方法激活AQS队列里面被阻塞的一个线程(thread)
* 被激活的线程则使用tryAcquire尝试,看当前状态变量state是否能满足自己的需要,
* 满足则线程被激活,然后继续运行,否则放入AQS队列挂起
*/
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
独占式同步状态获取流程,也就是acquire(int arg)
方法获取同步状态,如下图
共享式
多个线程可以同时执行。Semaphore
、CountDownLatch
、 CyclicBarrier
、ReadWriteLock
共享方式下,获取和释放资源的流程如下。
//首先使用tryAcquireShared尝试获取资源,具体是设置状态变量state的值。
//成功则直接返回,失败则将当前线程封装为类型Node.SHARED的Node节点后插入
//到AQS阻塞队列的尾部,并使用LockSupport.park(this)方法挂起自己
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
//尝试使用tryReleaseShared操作释放资源,这里是设置状态变量state的值,然后使用 //LockSupport.unpark(thread)方法激活AQS队列里面被阻塞的一个线程(thread)
//被激活的线程则使用tryAcquire尝试,看当前状态变量state是否能满足自己的需要,
//满足则线程被激活,然后继续运行,否则放入AQS队列挂起
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
入队源码解析
当一个线程获取锁失败后该线程会被转换为Node
节点,然后就会使用enq(final Node node)
方法将该节点插入到AQS
的阻塞队列;
private Node enq(final Node node) {
for (;;) {
Node t = tail;//(代码1)
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))//(代码2)
tail = head;
} else {
node.prev = t;//(代码3)
if (compareAndSetTail(t, node)) {//(代码4)
t.next = node;
return t;
}
}
}
}
接下来,我们通过节点图和代码来讲解一下入队的过程;
- 第一次循环:当要在
AQS
队列尾部插入元素时,AQS
队列状态如下图中的(default)
所示。也就是队列头、尾结点都指向null
;当执行代码1
后节点t
指向了尾部节点,这时候队列状态如下图中(I)
所示。这时候t
为null
,故执行代码2
,使用CAS
算法设置一个哨兵节点为头节点,如果CAS
设置成功。则让尾结点也指向哨兵节点,这时候队列状态如下图的(II)
所示。到此只插入了一个哨兵节点,还需要插入node
节点; - 所以第二次循环后执行到
代码1
,这个时候队列状态如下图(III)
所示;然后执行代码3
设置node
的前驱节点为尾部节点,这个时候队列状态如下图(IV)
所示,然后通过CAS
算法设置node
节点为尾部节点,CAS
成功后队列状态如下图中(V)
所示;CAS
成功后再设置原来的尾部节点的后驱节点为node
,这时候就完成了双向链表的插入,此时队列状态如下图的(VI)
所示;
acquireQueued()
方法解析
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//判断是否应该park
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
节点进入同步队列之后,就进入了一个自旋的过程,每个节点(或者说每个线程)都在自身观察,当条件满足,获取到了同步状态,皆可以从这个自旋过程中退出,否则依旧留在这个自旋的过程中。
if (p == head && tryAcquire(arg))
可以看到只有前驱节点是头节点才会去获取同步状态;
shouldParkAfterFailedAcquire
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//获取上一个节点的状态
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
//上一个节点都在阻塞,那么自己也阻塞好了
return true;
//>0表示取消状态
if (ws > 0) {
//上一个节点取消,那么重构删除前面所有取消的节点,返回到外层循环重试
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//这次还没有阻塞
//但下次如果重试不成功,则需要阻塞,这时需要设置上一个节点状态为 Node.SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
AQS
的实现
需要注意的是,对于独占锁tryAcquire
,tryRelease
。共享锁tryAcquireShare
,tryReleaseShare
。AQS
类都没有提供可用的实现。需要具体的子类来实现。子类要根据具体场景使用CAS
算法修改state
状态值,成功则返回true
,失败返回false
。子类还需要定义,在调用acquire
和release
方法时state
状态值的增减代表什么含义;
正如前面介绍的
state
状态信息在不同子类中代表的不同含义;
AQS
实现的锁除了需要重写上面介绍的方法外,还需要重写isHeldExclusively
方法,来判断锁是被当前线程独占还是被共享;
所以当我们想要自定义一个同步器,需要实现的步骤就很清晰了;
AQS
的底层是 使用的 模板方式 设计模式,使用者需要继承同步器并重写指定的方法;
创建一个自定义的同步器:
- 使用者继承
AbstractQueuedSynchronizer
并重写指定的方法。(共享资源state
的获取与释放)- 将
AQS
组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法
AQS
使用了模板方法模式,自定义同步器时需要重写下面几个AQS
提供的模板方法isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。 tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。 tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。 tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。 tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。
以
ReentrantLock
为例,state
初始化为0,表示未锁定状态。A线程lock()
时,会调用tryAcquire()
独占该锁并将state+1
。此后,其他线程再tryAcquire()
时就会失败,直到A线程unlock()到state=0(即释放锁)
为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加)
,这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state
是能回到零态的。再以
CountDownLatch
以例,任务分为N个子线程去执行,state
也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()
一次,state
会CAS(Compare and Swap)
减1。等到所有子线程都执行完后(即state=0
),会unpark()
主调用线程,然后主调用线程就会从await()
函数返回,继续后续动作。一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现
tryAcquire-tryRelease
、tryAcquireShared-tryReleaseShared
中的一种即可。但AQS
也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock
。
自定义同步组件;
同一时刻只允许一个线程占有(不可重入的独占锁)
//一次只能一个线程访问
public class AQSDemo1 implements Lock {
private static class Sync extends AbstractQueuedSynchronizer{
//是否处于占用
@Override
protected boolean isHeldExclusively() {
return getState()==1;
}
//状态为0时获取锁
@Override
protected boolean tryAcquire(int arg) {
if (compareAndSetState(0,1)){
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
//释放锁状态设置为0
@Override
protected boolean tryRelease(int arg) {
if (getState()==0) throw new IllegalMonitorStateException();
setExclusiveOwnerThread(null);
setState(0);
return true;
}
//提供条件变量接口
Condition newCondition(){return new ConditionObject();}
}
//仅需要将操作代理到Sync上即可
private final Sync sync = new Sync();
@Override
public void lock() {
sync.acquire(1);
}
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1,unit.toNanos(time));
}
@Override
public void unlock() {
sync.release(1);
}
@Override
public Condition newCondition() {
return sync.newCondition();
}
}
上述案例,通过调用同步器的
acquire(int args)
等模板方法,大大降低了实现一个自定义同步组件的门槛;
自定义同步组件TwinsLock
同一时刻,只允许至多两个线程同时访问,超过两个线程的访问将被阻塞;
确定访问模式:同一时刻支持多个线程访问,显然是共享式,需要使用
acquireShared(int args)
等系列方法;定义资源数:至多两个线程同时访问,表明同步资源数为2.设置初始
status为2
,当一个线程进行获取,status减1
。状态的合法范围为(0,1,2)当显示为0时,表示两个线程已经获取了同步资源,此时其他的线程只能阻塞;
//同一时刻,只允许至多两个线程同时访问,超过两个线程的访问将被阻塞;
public class TwinsLock implements Lock {
private final Sync sync = new Sync(2);
private static final class Sync extends AbstractQueuedSynchronizer{
Sync(int count){
if(count<=0){
throw new IllegalArgumentException("count must large than zero");
setState(count);
}
}
@Override
protected int tryAcquireShared(int reduceCount) {
for(;;){
int current = getState();
int newCount = current - reduceCount;
if(newCount < 0 || compareAndSetState(current,newCount)){
return newCount;
}
}
}
@Override
protected boolean tryReleaseShared(int returnCount) {
for(;;){
int current = getState();
int newCount = current + returnCount;
if(compareAndSetState(current,newCount)){
return true;
}
}
}
}
//仅需要将操作代理到Sync上即可
private final Sync sync = new Sync();
@Override
public void lock() {
sync.acquireShared(1);
}
@Override
public void unlock() {
sync.releaseShared(1);
}
//其他接口方法略
}
测试demo
public class TwinsLockTest{
@Test
public void test(){
final Lock lock = new TwinsLock();
//启动10个线程
for (int i = 0; i < 10; i++) {
Worker w = new Worker();
w.setDaemon(true);
w.start();
}
//每隔一秒换行
for (int i = 0; i < 10; i++) {
SleepUtils.second(1);
System.out.println();
}
class Worker extends Thread{
@Override
public void run() {
while(true){
lock.lock();
try{
SleepUtils.second(1);
System.out.println(Thread.currentThread().getName());
SleepUtils.second(1);
}finally{
lock.unlock();
}
}
}
}
}
}
引用
《Java并发编程之美》
《Java并发编程的艺术》