AQS 原理
AbstractQueuedSynchronizer
抽象同步队列简称 AQS
,它是实现同步器的基础组件,类图如下:
由上图可知,AQS
是一个 FIFO
的双向队列,其内部通过节点 head
和 tail
记录的队首和队尾元素,队列元素的类型为Node
。其中Node
的 thread
变量来存放进入AQS 队列里面的线程。Node
内部的 SHARED
用来标记该线程是获取共享资源时被阻塞挂起后放入 AQS
队列中,EXCLUSIVE
是用来标记线程是获取独占资源时被挂起后放入AQS
队列的; waitStatus
记录当前线程等待状态,可以为 CANCELLED
(线程被取消了),SIGNAL
(线程需要被唤醒),CONDITION
(线程在条件队列里面等待),PROPAGATE
(释放共享资源时需要通知其他节点);prev
记录当前节点的前驱节点,next
记录单前节点的后继节点。
state
作用
在 AQS
中维持了一个单一的状态信息 state
可以通过 getState() , setState(int newState),compareAndSetState(int expect, int update)
修改其值。
- 对于
ReentrantLock
来说,state
可以用来表示当前线程获取锁的可重入次数 - 对于
ReentrantReadWriteLock
来说,state
的高16为表示读状态,也就是获取该读锁的次数,低16位表示获取到写锁的线程的可重入次数。 - 对于
Semaphore
来说,state
是用来表示可用信号的个数。 - 对于
CountDownLatch
来说,state
用来表示计数器当前的值。
ConditionObject
作用
AQS
有个内部类 ConditionObject
, 用来结合锁实现线程同步。ConditionObject
可以直接访问 AQS
对象内部的变量,比如 state 状态值和 AQS
队列。ConditionObject
是条件变量,每个条件变量对应一个条件队列(单向链表队列),其用来存放调用条件变量的await
方法后被阻塞的线程。如类图所示,这个条件度列的头、尾元素分别为 firstWaiter
和 lastWaiter
。
对于 AQS
来说,线程同步的关键是对状态值 state
进行操作。根据 state
是否属于一个线程,操作 state
的方式分为独占方式和共享方式。
独占方式,获取与释放资源的流程
使用独占方式获取的资源时与具体线程绑定的,如果一个线程获取到了资源,就会标记是这线程获取到了,其它线程再尝试操作 state
获取资源时,会发现当前资源不是自己持有,就会在获取失败后被阻塞。如:ReentrantLock
的实现,当一个线程获取了 ReentrantLock
的锁后,在AQS
内部会首先使用CAS
操作将 state
状态值从0 改为1,然后设置当前锁的持有者为当前线程,当前线程再次获取锁时发现它就是锁的持有者,则会把 state 状态值从1改为2,也就是设置锁的重入次数。而当另外一个线程获取锁时发现自己并不是该锁的持有者就会被放入 AQS
阻塞队列后挂起
acquire(int arg)
获取资源
当一个资源调用 acquire(int arg) 方法获取独占资源时,首先使用 tryAcquire(arg) 方法尝试获取资源。
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
release(int arg)
释放资源
当一个线程调用 release(int arg) 方法时,首先使用 tryRelease(arg) 操作释放资源。
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
独占锁总结
继承自AQS 实现的独占锁 ReentrantLock
, 定义当 state
为 0 时表示锁空闲,为1时表示锁已经被占用。在重写 tryAcquire(int acquires)
时,在内部需要使用CAS 算法查看当前state
是否为0,如果为0则使用CAS 设置为1,并设置当前锁的持有者为当前线程,而后返回true,如果CAS 失败则返回false。在重写 tryRelease(int releases)
时,在内部需要使用CAS 算法把 state
的值从1改为0,并设置当前锁的持有者为 null
,然后返回true,CAS 操作失败则返回false。
共享方式,获取与释放资源的流程
共享方式的资源与具体线程是不相关的,当多个线程去请求资源时通过CAS 方式去竞争获取资源,当一个线程获取到了资源后,另外一个线程再次去获取时如果当前资源还能满足它的需要,则当前线程只需要使用CAS 方式进行获取。比如 Semaphore
信号量,当一个线程通过 acquire(int arg)
获取信号量时,会首先看当前信号量个数是否满足需要,不满足则把当前线程放入阻塞队列,如果满足则通过自旋 CAS 获取信号量。
acquireShared(int arg)
获取资源
当线程调用 acquireShared(int arg)
获取共享资源时,会首先使用 tryAcquireShared(int arg)
尝试获取资源,设置state的值,成功则直接返回,失败则将当前线程封装类型为Node.SHARED
的 Node
节点后插入到 AQS
阻塞队列的尾部,并使用LockSupport.park(this)
方法挂起自己。
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
releaseShared(int arg) 释放资源
当一个线程调用 releaseShared(int arg)
时会尝试使用tryReleaseShared
操作释放资源,这里设置状态变量 state
的值,然后使用LockSupport.unpark(thread)
激活AQS
队列里面被阻塞的一个线程。被激活的线程则使用 tryReleaseShared
查看当前状态变量state
的值是否能满足自己的需要,满足则该线程被激活,然后继续向下运行,否则还是会被放入AQS
队列并被挂起。
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
注意点
AQS 没有提供可用的 tryAcquireShared(int arg)
和 tryReleaseShared(int arg)
方法,正如 AQS 是锁阻塞和同步器一样,tryAcquireShared
和 tryReleaseShared
需要由具体的子类来实现。子类在实现 tryAcquireShared
和 tryReleaseShared
是要根据具体场景使用 CAS 算法尝试修改 state 状态值,成功则返回true,否则返回false。如继承自AQS 实现的读写锁 ReentrantReadWriteLock
里面的读锁在重写 tryAcquireShared
时,首先查看写锁是否被其他线程持有,如果是则直接返false,否则使用CAS 递增 state 的高16位,读锁在重写 tryReleaseShared
时,在内部需要使用CAS 算法把当前state 的值高16位减1,成功则返回true,失败则返回false。