AbstractQueuedSynchronizer 主要数据模型:
AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer
implements java.io.Serializable {
static final class Node {...};
public class ConditionObject implements Condition, java.io.Serializable{...};
private transient volatile Node head; // 头节点
private transient volatile Node tail; // 尾节点
private volatile int state;
private static final Unsafe unsafe = Unsafe.getUnsafe();
......
private Node enq(final Node node){...}; //通过自旋操作把当前节点加入到队列中
private Node addWaiter(Node mode){...}; // 加入同步队列
private void doReleaseShared(){...};
private void cancelAcquire(Node node){...};
......
}
static final class Node {
/** 指示节点正在共享模式下等待的标记 */
static final Node SHARED = new Node();
/** 指示节点正在以独占模式等待的标记 */
static final Node EXCLUSIVE = null;
/** waitStatus值,指示线程已取消 */
static final int CANCELLED = 1;
/** waitStatus值,指示后续线程需要释放 */
static final int SIGNAL = -1;
/** waitStatus值,指示线程正在等待队列(condition)中 */
static final int CONDITION = -2;
/** waitStatus值,指示下一个acquireShared应该无条件传播 */
static final int PROPAGATE = -3;
/**
* 状态值,取值如下:
* SIGNAL: 值为-1,被标识为该等待唤醒状态的后继结点,当其前继结点的线程释放了同步锁或被取消,将会通知该后继结点的线程执行。简言之,就是处于唤醒状态,只要前继结点释放锁,就会通知标识为SIGNAL状态的后继结点的线程执行。
* CANCELLED:值为1,在同步队列中等待的线程等待超时或被中断,需要从同步队列中取消该Node的结点,其结点的waitStatus为CANCELLED,即结束状态,进入该状态后的结点将不会再变化。
* CONDITION:值为-2,与Condition相关,该标识的结点处于等待队列中,结点的线程等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
* PROPAGATE:值为-3,与共享模式相关,在共享模式中,该状态标识结点的线程处于可运行状态。
* 0状态:值为0,代表初始化状态。
**/
volatile int waitStatus;
volatile Node prev; // 前驱节点
volatile Node next; // 后继节点
volatile Thread thread; // 当前线程
Node nextWaiter; // 存储在condition队列中的后继节点
final boolean isShared() { // 是否为共享锁
return nextWaiter == SHARED;
}
/** 获取前置节点 */
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() { // Used to establish initial head or SHARED marker
}
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
AQS 的功能分为两种:独占和共享
独占锁,每次只能有一个线程持有锁,ReentrantLock就是以独占方式实现的互斥锁
共享锁, 允许多个线程同时获取锁,并发访问共享资源,比如ReentrantReadWriteLock
private volatile int state; 这是AQS中的一个属性,不同的实现中所表达的含义不一样,对于重入锁的实现来说,表示一个同步状态。它有两个含义的表示
1. 当 state=0 时,表示无锁状态
2. 当 state>0 时,表示已经有线程获得了锁,代表重入次数
AQS内部维护着一个FIFO队列,该队列就是CLH同步队列,CLH同步队列是一个FIFO双向队列
入列:
出列:
图解
假设3个线程(A、B、C)来争抢锁,过程如下: 下面的图最后的是C线程,不是B线程,画错了
获取锁acquire
shouldParkAfterFailedAcquire方法修改当前节点的前置节点waitStatus为SIGNAL(-1),修改成功后parkAndCheckInterrupt方法挂起当前线程(下图标红表示被挂起LockSupport.park)
释放锁release
unparkSuccessor将头节点状态更新为0,并唤醒后置节点(下图为B)
acquireQueued方法中被阻塞的某个线程(B)被唤醒,获得资源锁住,重置头节点
独占式同步状态获取
- tryAcquire:去尝试获取锁,获取成功则设置锁状态并返回true,否则返回false。该方法自定义同步组件自己实现,该方法必须要保证线程安全的获取同步状态。
- addWaiter:如果tryAcquire返回FALSE(获取同步状态失败),则调用该方法将当前线程加入到CLH同步队列尾部。
- acquireQueued:当前线程会根据公平性原则来进行阻塞等待(自旋),直到获取锁为止;并且返回当前线程在等待过程中有没有中断过。
- selfInterrupt:产生一个中断。
AQS.acquireQueued方法思路
1. 获取当前节点的 prev 节点
2. 如果 prev 节点为 head 节点,那么它就有资格去争抢锁,调用 tryAcquire 抢占锁
3. 抢占锁成功以后,把获得锁的节点设置为 head,并且移除原来的初始化 head节点
4. 如果获得锁失败,则根据 waitStatus 决定是否需要挂起线程
5. 最后,通过 cancelAcquire 取消获得锁的操作
1. 如果 ThreadA 的 pred 节点状态为 SIGNAL,那就表示可以放心挂起当前线程
2. 通过循环扫描链表把 CANCELLED 状态的节点移除
3. 修改 pred 节点的状态为 SIGNAL,返回 false.
返回 false 时,也就是不需要挂起,返回 true,则需要调用 parkAndCheckInterrupt挂起当前线程
使用 LockSupport.park 挂起当前线程变成 WATING 状态
Thread.interrupted,返回当前线程是否被其他线程触发过中断请求,也就是thread.interrupt(); 如果有触发过中断请求,那么这个方法会返回当前的中断标识true,并且对中断标识进行复位表示已经响应过了中断请求。如果返回 true,意味着在 acquire 方法中会执行 selfInterrupt()。
标识如果当前线程在 acquireQueued 中被中断过,则需要产生一个中断请求,原因是线程在调用 acquireQueued 方法的时候是不会响应中断请求的
acquire(int arg)方法流程图如下:
独占式获取响应中断
独占式超时获取
doAcquireNanos(int arg, long nanosTimeout)流程图:
假设 3 个线程来争抢锁,那么截止到 enq 方法运行结束之后,或者调用 addwaiter方法结束后,AQS 中的链表结构图:
上图尾节点ThreadB=ThreadC
共享式同步状态获取
共享式同步状态释放
LockSupport
LockSupport是用来创建锁和其他同步类的基本线程阻塞原语
当需要阻塞或者唤醒一个线程的时候,AQS都是使用LockSupport这个工具类来完成的。
基本流程:
1.尝试获取锁
2.失败则创建Node节点,加入同步队列(第一次加入时,会创建一个空的节点作为头节点,当前结点作为尾结点)
3.判断当前节点的前节点是否是头节点,是的话尝试获取锁,获取成功则将当前结点置为头节点并结束,否则执行4
4.如果获取失败或不是头节点,则调用LockSupport.park进入阻塞;等待占有锁的资源释放lock.release(LockSupport.unpark)后自旋获取锁
5.最终处理,如果异常抛出则取消当前节点,主题置为CANCLED,从同步队列中移除
锁释放过程:
为什么在释放锁的时候是从 tail 进行扫描
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;//获得 head 节点的状态
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);// 设置 head 节点状态为 0
Node s = node.next;//得到 head 节点的下一个节点
if (s == null || s.waitStatus > 0) {
//如果下一个节点为 null 或者 status>0 表示 cancelled 状态.
//通过从尾部节点开始扫描,找到距离 head 最近的一个waitStatus<=0 的节点
s = null;
for (Node t = tail; t != null && t != node; t =t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null) //next 节点不为空,直接唤醒这个线程即可
LockSupport.unpark(s.thread);
}
在 cas (步骤2)操作之后,t.next=node (步骤3)操作之前。存在其他线程调用 unlock 方法从 head 开始往后遍历,由于 t.next=node 还没执行意味着链表的关系还没有建立完整。就会导致遍历到 t 节点的时候被中断。所以从后往前遍历,一定不会存在这个问题。
AQS共享锁与独占锁的对比
共享锁的实现稍比独占锁复杂,但大同小异。二者对比如下:
独占锁:
独占锁是只有头节点获取锁,其余节点的线程继续等待,等待锁被释放后,才会唤醒下一个节点的线程;
独占锁的同步状态state值在0和1之间切换,保证同一时间只能有一个线程是处于活动的,其他线程都被阻塞,参考ReentranLock。
独占锁是一种悲观锁。
独占锁逻辑总结:
线程获取锁时,根据 state 值判断是否能获取锁;获取失败,则封装成节点加入同步队列尾部,将前驱节点的 waitStatus 设置为 SIGNAL,然后调用 LockSupport.park() 挂起当前线程; 当其他线程调用 release 方法释放锁时,将 state 置为 0,找到头节点的后继节点,使用 LockSupport.unpark() 唤醒它去争抢锁;争抢成功则将该节点设为新的头节点。
共享锁:
共享锁是只要头节点获取锁成功,就在唤醒自身节点对应的线程的同时,继续唤醒AQS队列中的下一个节点的线程,每个节点在唤醒自身的同时还会唤醒下一个节点对应的线程,以实现共享状态的“向后传播”,从而实现共享功能。
共享锁的同步状态state值在整数区间内(自定义实现),如果state值<0则阻塞,否则不阻塞。参考ReadWriteLock、Semphore、CountDownLautch等。
共享锁是一种乐观锁,允许多个线程同时访问共享资源。
共享锁逻辑总结:
线程获取共享锁时,根据 state(可用资源数)判断是否能获取;获取失败(state≤0),则封装成 SHARED 类型节点加入同步队列尾部,将前驱节点的 waitStatus 设置为 SIGNAL,然后调用 LockSupport.park() 挂起当前线程;
当其他线程调用 release 方法释放共享锁时,增加 state(归还资源),找到头节点的后继共享节点,使用 LockSupport.unpark() 唤醒它去争抢锁;该线程争抢成功(扣减 state)后,会继续唤醒自身的后继共享节点(传播唤醒),最终抢到锁的节点设为新的头节点。
countdownLatch调用release时是扣减state直至0,Semaphore调用release时是增加state值
核心差异强化(对比独占锁)
- 独占锁:state 是 “锁占用状态”,释放时只唤醒一个后继节点;
- 共享锁:state 是 “可用资源数”,释放后唤醒的节点会传播唤醒后续共享节点(链式唤醒)。