总体介绍
基于队列的抽象同步器,它是jdk中所有显示的线程同步工具的基础,像ReentrantLock/DelayQueue/CountdownLatch等等,都是借助AQS实现的。Java中已经有了synchronized关键字,那么为什么还需要来这么一出呢?因为AQS能实现更多维度,更多场景的锁机制,例如共享锁(读锁)/基于条件的线程阻塞/可以实现公平和非公平的锁策略/可以实现锁等待的中断,而synchronized关键字由JVM实现,在代码使用层面来说,如果仅仅是使用独占锁,那synchronized关键字比其它的锁实现用起来方便。
下面步入正题,来看看AQS都提供了哪些能力。
1、主体结构
先来看看内部的主体结构:
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
protected AbstractQueuedSynchronizer() { }
//内部类Node,代表每一个获取或者释放锁的线程
static final class Node{}
//head 和 tail构成了同步队列链表的头节点和尾节点。
private transient volatile Node head;
private transient volatile Node tail;
//同步状态值,所有的同步行为都是通过state这个共享资源来实现的。
private volatile int state;
//条件对象,用于同步在当前锁对象上的线程
public class ConditionObject implements Condition, java.io.Serializable{}
/******
一系列的内部方法:
包括尝试获取锁权力
尝试获取锁失败后构造节点放入等待队列
各种入队出队的操作
尝试释放锁等
********/
//一系列的Unsafe操作
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long stateOffset;
private static final long headOffset;
private static final long tailOffset;
private static final long waitStatusOffset;
private static final long nextOffset;
}
所以总结起来AQS就是通过一系列的Unsafe方法去操作一个链表队列,而链表队列中每个节点需要操作的共享资源就是一个int state字段,如果要用到条件等待,则需要了解ConditionObject。
2、节点类的构造
再来看看Node节点的内部结构:
static final class Node {
//常量,见nextWaiter属性
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
//waitStatus 常量值。表示等待获取锁的线程已经被取消(线程中断/等待超时)
static final int CANCELLED = 1;
//waitStatus 常量值。表示后继线程需要unpark
static final int SIGNAL = -1;
//waitStatus 常量值。表示线程正在Condition队列中
static final int CONDITION = -2;
//waitStatus 常量值。表示线程的acquireShared行为需要无条件的向队列的下一个节点传递。用在共享锁的场景。
static final int PROPAGATE = -3;
//注意,waitStatus除了以上常量值以为,由于是int类型,则默认是0
volatile int waitStatus;
//前继节点
volatile Node prev;
//后继节点
volatile Node next;
//节点所代表的线程
volatile Thread thread;
//如果节点再Condition等待队列中,则该字段指向下一个节点,如果节点在同步队列中,则为一个标志位,其值为 SHARED或者null
Node nextWaiter;
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;
}
}
所以在同步队列中Node应该长下面这个样子:
同步队列
在Condition等待队列中应该长如下这个样子:
等待队列
Node节点一共有3个构造方法他们分别在同步队列初始化时/加入同步队列/加入Condition队列时使用,结合构造方法以及AQS和Condition类,我们看下最终队列会长什么样子。
首先看一下无参的构造方法在什么地方使用:
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode); //用当前线程和mode类型构造节点,此时waitStatus默认为0
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) {
Node t = 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;
}
}
}
}
addWaiter方法是将线程放入队列的方法,开始构造new Node(Thread.currentThread(), mode)这样的节点,然后尝试入队列,如果入队列失败则进入enq方法,在enq中,如果队列为空(当t==null的时候,因为入队操作是从队尾进行的),此时构造了一个空的Node节点,然后将head和tail指向它。我们称这个节点为哨兵节点,为什么需要这样一个节点,后面会说明,哨兵节点入队列后我们通过当前线程构造的的节点new Node(Thread.currentThread(), mode)再入队列。由于构造方法没有传入waitStatus值,所以此时waitStatus默认为0。
所以综合AQS来看,最终的同步队列应该长如下这个样子:
同步队列中Node节点的存放情况那么在Condition等待队列中呢? Condition队列中有firstWaiter和lastWaiter分别指向头节点和尾节点。
先看下加入Condition队列的代码:
private Node addConditionWaiter() {
Node t = lastWaiter;
// If lastWaiter is cancelled, clean out.
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
//构造waitStatus等于CONDITION的NODE节点
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null) //如果队列为空,则新节点入队列,头节点指向新节点
firstWaiter = node;
else
t.nextWaiter = node; //如果队列不为空,则新节点加入到队列末尾
lastWaiter = node; //尾节点指向新节点
return node;
}
所以,最终Condition队列中的节点如下:
Condition队列中的节点
Condition队列中的节点是单向的,并且没有哨兵节点,在Condition队列中,nextWaiter指向下一个节点,而不是像在同步队列中那样指向模式(SHARED, null),其中各个节点的waitStatus等于CONDITION(-2)。
3、获取锁
3.1 首先看写锁lock的实现
public void lock() {
sync.acquire(1); //调用同步器的acquire方法
}
这里同步器由ReentrantReadWriteLock来继承AQS后具体实现,不同的锁对AQS的实现是不一样的。
public final void acquire(int arg) {
if (!tryAcquire(arg) && //尝试获取锁失败
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) //并且节点入队列成功
selfInterrupt(); //线程中断
}
整个acquire方法其实就是3部曲,①尝试获取锁 ②如果获取锁没有成功,则构造节点加入等待队列中 ③如果节点入队列成功,则线程自我中断让出资源。当线程进入同步队列中后就实现了获取锁过程中的线程阻塞功能了,因为这个时候线程是自我中断状态。
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
我们找到tryAcquire方法发现,它直接抛出了一个异常。这里其实是AQS用了模板模式,将tryAcquire的具体实现预留给各种锁来实现,例如单纯的写锁只用判断state是否为0,而读写锁的实现是要先取state的高16或者低16位来判断。所以不同的锁对tryAcquire的实现不同,但是不管如何实现,由于state是共享变量,所以各个实现一定会保证state的线程安全。
3.1.1 线程加入同步队列
当获取锁失败时,就开始构造节点入队列了。
//在写锁的情况下,这里mode为Node.EXCLUSIVE,其实就是null
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
//在enq之前的这段代码是一个快读入队列的实现,如果入队不成功则交由enq来实现
Node pred = tail; //缓存tail节点,因为从队尾加入节点,所以理论上tail就是前继节点
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) { //当队尾不等于空,则用CAS方法修改tail节点,让其指向本次的构造的新节点。
pred.next = node; //如果CAS成功则说明入队成功,然后将前继节点的next指向新节点。
return node;
}
}
//如果队列为空,或者新节点入队列失败,则交给enq处理。
enq(node);
return node;
}
从上面的代码我们能看出AQS中一个很重要的逻辑:入队列,一定是优先保证tail的重新指向,然后才是前节点的next指向新节点。如果拿分布式系统数据一致性举例的话就是:tail是数据强一致性,而next是最终一致,这一点在后面的逻辑判断中很重要。
//自旋的入队列方法
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize //队列为空
if (compareAndSetHead(new Node())) //构造空节点,CAS改变头节点
tail = head; //将tail指向新节点
} else {
node.prev = t;
if (compareAndSetTail(t, node)) { //队列不为空,则将tail指向新节点
t.next = node; //前节点的next指向新节点
return t;
}
}
}
}
这里队列不为空的情况与addWaiter的快速入队逻辑是一样的没什么好说的,这里的重点是队列等于空的时候,我们可以看到代码构造了一个不包含任何线程的“空”节点,然后将head指向它,这里有两个问题: 1.为什么要构造这个空节点,直接用新节点作为头节点不好么? 2.为什么队列为空的时候是先修改head而不是像队列不为空的时候那样直接修改tail呢。
这里先保留这两个问题,后续再来解答。
这一节主要记住的一个核心点就是:队列是否为空通过tail判断,一个节点加入队列分为两步,第一步是强一致性的修改tail(节点的pre已经指向的前节点),下一步再是修改前节点的next。
3.1.2、进入同步队列后继续尝试获取锁或者睡眠
线程被构造为节点进入队列后,接下来就是对队列中的节点进行获取锁的处理:
//独占模式并且不可中断的为同步队列中的线程获取锁
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); //如果获取锁成功,则将头节点指向获取锁成功的节点,并清空该节点的thread和pre,让该节点变成新的哨兵
p.next = null; // help GC
failed = false;
return interrupted; //返回中断状态
}
//如果获取锁失败,或者当前节点不是最靠前的非哨兵节点,则尝试将线程park
if (shouldParkAfterFailedAcquire(p, node) && //判断当前节点是否可以park,如果可以则尝试park
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node); //如果获取锁失败,取消竞争锁
}
}
//判断当前节点是否可以睡眠(park)
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL) //如果前继节点的waitStatus等于Node.SIGNAL,则前节点会负责唤醒当前节点, 当前节点可以park
return true;
if (ws > 0) { //如果前节点已经取消(被中断或超时)
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0); //直到遍历到非取消的前节点为止
pred.next = node;//将遍历到的前节点next指向node节点
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
//将前节点waitStatus更新为Node.SIGNAL,表示后继节点需要unpark
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
//park当前线程,并返回当前线程的中断状态
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
看到这里,我们来说一下刚才遗留的那个问题:为什么要增加一个哨兵节点。如果没有哨兵,如上图,这个时候该节点获取到锁,这个时候需要做两件事情,先判断head是否等于tail,然后需要将head指向当前的next,其实就是指向null,tail也需要指向null,这个时候,如果有另外一个线程正在入队列,需要将head或者tail指向这个入队列新节点,这个时候如果入队节点刚完成tail以及pre的指向,还没来及更改前节点的next,这时出队列这边则会将head指向null,tail也会被指向null,当然这里如果在将tail指向null之前进行一次当前tail与老tail的对比, 如果一致则更新为null,不一致则不更新能避免tail指向null,但是head这边就显得比较麻烦了,每次获取锁成功要重新设置head都需要从队尾向对头遍历,以防止有新的节点放入,直到遍历到节点中thead为空的(获取锁已出队列)节点的后一个节点为止。 这样的处理显得相当的麻烦。
无哨兵的情况
无哨兵节点获取到锁后修改head和tail的过程
第二个问题是为什么要先修改head而不能直接修改tail,先看有哨兵的情况,我改写了一下enq的方法:
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize //队列为空
Node n = new Node();
if (compareAndSetTail(n)) //构造空节点,CAS改变头节点
head = n;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) { //队列不为空,则将tail指向新节点
t.next = node; //前节点的next指向新节点
return t;
}
}
}
}
这样的处理方式我认为它是线程安全的。在没有哨兵的情况下就是把n换成node,逻辑是一样的。
3.1.3 获取锁或者睡眠过程中线程被中断(取消获取锁)
我们再来看看取消获取锁做了什么事情:
//取消获取锁
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null) //空判断
return;
node.thread = null; //取消节点对线程的引用
//将node节点往前继节点方向所有连续的取消状态的节点出队列
// Skip cancelled predecessors
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// predNext is the apparent node to unsplice. CASes below will
// fail if not, in which case, we lost race vs another cancel
// or signal, so no further action is necessary.
Node predNext = pred.next;
// Can use unconditional write instead of CAS here.
// After this atomic step, other Nodes can skip past us.
// Before, we are free of interference from other threads.
node.waitStatus = Node.CANCELLED; //修改node状态
// If we are the tail, remove ourselves.
//如果当前节点是队尾,则将当前节点移除队列,这个时候就不用设置前节点状态
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null); //将前节点(这个时候已经是队尾节点)的next指向null,这里不保证它一定会成功,因为可能有其它新节点加入,用CAS方式避免将覆盖其它线程的操作。
} else {
// If successor needs signal, try to set pred's next-link
// so it will get one. Otherwise wake it up to propagate.
int ws;
if (pred != head && //如果当前节点不是队尾,也不是最靠前的节点
((ws = pred.waitStatus) == Node.SIGNAL || //前节点状态已经为Node.SIGNAL 或者 将前节点状态更改为Node.SIGNAL成功
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) { //并且前节点还未出队列
Node next = node.next;
if (next != null && next.waitStatus <= 0) //当前节点的next节点存在并且没有取消
compareAndSetNext(pred, predNext, next);//则将前节点的next指向node节点的next
} else {
unparkSuccessor(node); //唤醒后继节点 //node为头节点,或者更新前节点状态为SIGNAL失败(前节点就没办法自动唤醒后节点了),或者前节点thead==null(前节点已取消)
}
node.next = node; // help GC 后继节点指向自己,去除引用,帮助GC
}
}
其主要逻辑如下图:
当前节点是尾节点的情况
尾节点直接出队列,tail指向pred节点。
当前节点非首节点也非尾节点这两个图相同的逻辑就在于将当前节点状态修改为取消,然后thead置空,最后出队列,但是这里没有重新设置当前节点的prev指针,如果从tail向前遍历,还是能遍历到node节点。
3.1.4 需要唤醒后继节点的情况
我们再来看下,当前节点状态更新为SIGNAL失败(这是哪种情况),或者当前节点就是首节点时,唤醒后继节点是如何操作的:
//唤醒node节点的后继节点
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
//在唤醒后继之前,先将node节点状态改为0
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
//如果node的后继节点被取消,则从队尾向node节点遍历,找到距离node节点最近的waitStatus<=0的节点,然后唤醒s节点
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);
}
这里有一个问题就是取消当前节点获取锁,然后要唤醒后继节点的时机,为什么需要唤醒后继节点?
我们再看下这段代码:
if (pred != head && //如果当前节点不是队尾,也不是最靠前的节点
((ws = pred.waitStatus) == Node.SIGNAL || //前节点状态已经为Node.SIGNAL 或者 将前节点状态更改为Node.SIGNAL成功
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
……..
} else {
unparkSuccessor(node);
}
其中if判断为false的情况有:
-
pred == head : 前节点为head节点,说明当前节点为第一个有效节点,如果当前节点被中断了,head节点在唤醒后继节点时可能会找到老节点(当前移除的节点),所以需要手动唤醒node的后继节点。
-
pred != head的情况,pred.waitStatus != Node.SIGNAL 并且ws<=0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL) 为false :即node的前继节点非head的情况,前继节点状态不等于SIGNAL (ws>0即等于CANDLED,或者ws=[0, CONDITION, PROPAGATE])但是更新状态为SIGNAL失败,这里ws>0是让后继节点醒来,在acquireQueued方法中重新判断是否获取锁还是睡眠。其它情况将状态更新为SIGNAL失败,则说明前节点刚好完成锁释放并执行了unparkSuccessor,在该方法中,需要将当前释放锁节点的状态更新为0
-
当pred!=head,前继节点状态也等于SIGNAL的时候,pred.thread == null, 这种情况唯有前继节点为取消状态线程才会为空,如果前继节点已经取消,则应该唤醒后继节点,重新处理acquireQueued。
3.2 释放写锁
这里还是从ReentrantReadWriteLock可重入读写锁入手,查看它的释放写锁的实现:
public void unlock() {
sync.release(1);
}
可以看到,通过同步器调用release方法来达到释放锁的目的。
重点看一下AQS中release方法的实现:
public final boolean release(int arg) {
if (tryRelease(arg)) {//尝试释放锁(共享变量)
Node h = head;
if (h != null && h.waitStatus != 0) //释放锁成功后,唤醒同步队列中下一个需要获取锁的节点
unparkSuccessor(h);
return true;
}
return false;
}
这里tryRelease由具体的锁来实现。
4、获取读锁(共享锁)
还是以ReentrantReadWriteLock锁的实现为例,首先看下获取读锁的代码入口:
public void lock() {
sync.acquireShared(1);
}
很明显也是调用同步器来实现的,这里主要关注acquireShared。
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
整体逻辑与获取写锁类似,首先tryAcquireShared,该方法的具体实现逻辑有锁实现来决定,在tryAcquireShared中,锁的实现会根据state来判断,如果当前已经加了写锁,则不能加读锁,如果当前有其它线程获取的读锁,则本次同样能加读锁,并且会判断是否同一个线程,如果是,则重入次数加1。 如果获取锁不成功,则进入doAcquireShared方法。
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED); //构造同步队列节点并加入队列
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg); //如果前节点已经是head节点,则继续尝试tryAcquireShared
if (r >= 0) { //获取读锁成功
setHeadAndPropagate(node, r); //获取锁成功,并且唤醒后续需要获取共享锁的节点
p.next = null; // help GC
if (interrupted) //线程unpark后,会判断线程的中断状态,如果线程已经被中断,这里继承中断状态
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node); //获取共享锁过程中的任何异常都需要取消该节点继续获取锁,详情建写锁的取消逻辑。
}
}
共享锁的核心逻辑在于setHeadAndPropagate方法,该方法中实现了共享锁获取锁的冒泡。
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node); //把当前节点变成头节点, 并且将thread引用置空,prev置空
if (propagate > 0 ||
h == null ||
h.waitStatus < 0 ||
(h = head) == null ||
h.waitStatus < 0) { //唤醒后继节点中所有的共享模式的等待者
Node s = node.next;
if (s == null || s.isShared()) //后继节点有可能正在加入队列(在addWaiter中是cas保证tail成功,然后再设置的next引用),或者后继节点是共享模式,都需要尝试唤醒后继节点。
doReleaseShared(); //这里不是释放锁, 是唤醒后继节点。
//对于共享模式而言,前者获取到共享锁后,需要唤醒后继的共享锁等待者;这与前继节点释放锁后需要唤醒后继节点逻辑一致,所以作者把通通的共享模式下唤醒后继节点的行为封装为了一个方法。
}
}
从上面代码可以看出,判断是否需要继续往后传递获取锁的行为取决于紧邻的后继节点的模式是否为Shared。如果为Shared模式,则需要向后传递获取锁的行为,具体逻辑看doReleaseShared的实现。
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) { // 有至少一个后继节点
int ws = h.waitStatus;
if (ws == Node.SIGNAL) { //后继节点需要唤醒
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) //这里为什么一定需要CAS成功才能唤醒后继节点,因为在共享模式下,后继节点可能被唤醒后很快释放锁,如果前节点状态还是为SIGNAL,则等它释放锁时需要去唤醒后继节点,此时它的后继已经释放锁了,这里就会有问题,其实就是并发的问题。
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) //如果节点状态已经为0,则说明要么节点已经执行了unparkSuccessor,或者没有后继节点了。
continue; // loop on failed CAS
}
//如果在执行以上动作中,有新节点获取到锁(谁获取到锁,head指向谁),从新尝试唤醒新节点的后继节点。
if (h == head) // loop if head changed
break;
}
}
doReleaseShared方法是一个自旋方法,首先判断是否还有后继节点if (h != null && h != tail),如果有后继节点,拿到当前头节点状态,如果头节点状态为SIGNAL,则需要唤醒后继节点,这里后继节点可能是共享节点,也可能是一个独占节点(才挂上来的),我们注意到代码在执行唤醒后继节点unparkSuccessor之前,先执行了if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)),这里的逻辑显然于独占模式释放锁唤醒后继节点或者共享模式释放锁的逻辑不太一样,这里是强制优先讲头节点状态设置0,只有设置为0成功的线程才能执行唤醒下一个节点的操作。这里之所以要这样设计,是因为共享模式下,一次释放锁的线程可能有多个,甚至存在有的共享节点在释放锁,有的共享节点却在获取锁,但是不管怎么样,他们都会并发的调用doReleaseShared方法,如果这里不加以控制,则会重复的调用unparkSuccessor,最后会重复的调用unpark方法(这里我还没明白如果重复调用unpark会有什么问题)。
那么刚才没有compareAndSetWaitStatus(h, Node.SIGNAL, 0)成功的节点会再次循环,再次进来时发现此时头节点状态为0(因为其它线程执行了唤醒操作),则讲状态更新为PROPAGATE,这里重点说下这个逻辑,我翻遍了所有的代码都没有发现哪里有指定将PROPAGATE变更为其它状态的逻辑,所以我不指定作者这里设计一个这个状态有何用,我认为更不需要compareAndSetWaitStatus(h, 0, Node.PROPAGATE)这段逻辑,当其线程再次进入判断发现头节点状态不为SIGNAL时,直接结束。这种情况下再来看刚才的唤醒操作,当唤醒后的节点重新尝试获取锁失败时,它又会执行shouldParkAfterFailedAcquire方法,然后将前节点更改为SIGNAL,如果唤醒后的节点获取锁成功,则head节点指向该节点,后续的唤醒操作只与该节点有关。所以真心不明白为什么要有段设置为PROPAGATE的逻辑,主要是代码中任何地方看不到使用了PROPAGATE的这个逻辑。
这里再说下自旋,退出自旋的条件是if (h == head),即在处理过程中没有新的节点获取到锁,用反证法,假设有新的节点获取到锁,这种情况下如果退出了方法会有问题么 ? 我认为没有问题,因为获取到锁的节点最终会释放锁,释放锁的动作又会唤起后继节点,所以为什么要自旋呢?
这一段如果有看明白作者意图的还请指点一二。
这里我们可以来看下整个node节点的状态变化:
node节点状态变化通过上图观察,所有从0变到PROPAGATE的,最后都会因为新加入节点或者节点取消而变称SIGNAL,而SIGNAL最终又都会因为需要唤起后继节点而将当前节点更新为0,这形成了一个完整的闭环,那为什么不是直接0变为SIGNAL,不知道PROPAGATE在中间起到什么作用。
5、释放读锁
相较于获取读锁,释放的过程就比较简单了
public void unlock() {
sync.releaseShared(1); //调用同步器的releaseShared
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
可以看到, 在尝试获取锁成功后,直接调用doReleaseShared唤醒后继节点。这里的逻辑就与共享锁获取锁后需要继续唤醒紧邻的后继共享节点一样。
还有从这里就可以看出,doReleaseShared方法是被多个线程同时调用的,所以在代码里面unparkSuccessor处需要进行并发的控制。
6、带中断的获取锁
不管是读锁还是写锁,在获取锁的过程中还有一类获取方法是在等待锁的过程中允许线程被中断的,方法会抛出InterruptedException异常。
这里用共享锁的lockInterruptibly方法举例:
public void lockInterruptibly() throws InterruptedException {
sync.acquireSharedInterruptibly(1); //调用同步器的带中断的获取共享锁方法
}
public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
if (Thread.interrupted()) //首先检查线程的中断状态,如果线程已经被中断,则抛出异常,终止程序
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
接下来看下doAcquireSharedInterruptibly的实现:
private void doAcquireSharedInterruptibly(int arg)throws InterruptedException {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException(); //线程在等待过程中被中断,当线程醒来后判断到中断状态,直接抛出中断异常终止程序
}
} finally {
if (failed)
cancelAcquire(node);
}
}
doAcquireSharedInterruptibly与doAcquireShared方法相比,唯一的区别就在于线程被unpark后如何处理中断的,在doAcquireShared中,是将中断状态设置为一个boolean标志返回给调用者,让调用者自行决定该如何处理,而在doAcquireSharedInterruptibly中,如果发现线程被中断,直接抛出中断异常终止程序。
7、公平/非公平同步器
公平与非公平同步器其实与AQS本身无关,这都是AQS具体的实现,不同的锁有不同的实现方式,但是总体的规则是一致的。因为是AQS的实现,我想有必要这里说一下,具体的实现请参考各个锁的实现。
公平同步器:
所谓公平,即遵循先到先得的原则,谁先到达队列,则谁就优先获取锁,不得在刚尝试锁的时候不管队列中是否有等待中的节点直接竞争锁。
而所谓不公平其实就是当一个线程尝试获取锁的时候,不会主动的去判断同步队列中是否有等待的节点,直接就去竞争锁,如果竞争失败则进入同步队列进行等待。
8、ConditionObject条件等待
在AQS中还有一个比较重要的类:ConditionObject,这个类实现了在基于AQS锁的情况下对获取到锁的线程进行有条件的等待和唤醒,其主要的方法是await和signal以及它们的变种。有很多博客都拿它与Object对象的wait和notify作比较,说ConditionObject的await和signal运用的地方要比wait和notify广,其实并不然。它们使用的场景是截然不同的,不然的话Doug Lea也不会费这力气重新造一个轮子。
我认为他们的区别主要在于,Object的wait和notify其实是在任何情况下都是可以调用的,而ConditionObject的await和signal必须要在基于AQS的锁环境下才能调用,不然就会抛出异常(这也是我认为它们之间最大的差异);其次,ConditionObject是专门为AQS服务的,它的节点的构造,状态的标志等都与AQS有关,在wait操作和notify操作时都需要去操作AQS的同步队列。
所以综上所述,ConditionObject是专门为AQS服务的,而不像有的博客写的它的用途要比Object的实现要广。
接下来看看ConditionObject的具体实现。
public class ConditionObject implements Condition, java.io.Serializable {
private static final long serialVersionUID = 1173984872572414699L;
/** First node of condition queue. */
private transient Node firstWaiter;
/** Last node of condition queue. */
private transient Node lastWaiter;
/**
* Creates a new {@code ConditionObject} instance.
*/
public ConditionObject() { }
/***
methods
***/
}
它的核心结构就是有一个Node的头节点和尾节点,然后Node中通过Node nextWaiter进行关联形成了一个Condition链表队列,后续所有的操作都是围绕这个队列和同步队列来进行的。
8.1 await方法
public final void await() throws InterruptedException {
if (Thread.interrupted()) //判断当前线程是否被中断
throw new InterruptedException();
Node node = addConditionWaiter(); //①将当前线程构造为Condition等待节点并加入队列,详情见addConditionWaiter说明
int savedState = fullyRelease(node); //②释放锁现在全部的状态,锁可能有重入,所以这里不是直接调用AQS的release方法,详情见fullyRelease说明
int interruptMode = 0; //标记线程在await过程中的中断状态,0表示未中断
while (!isOnSyncQueue(node)) { //判断node节点是否在同步队列中,只有node节点进入了同步队列循环才会结束(即,被signal了)
LockSupport.park(this);//如果不在同步队列中, 则park当前线程
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) //③线程被唤醒或者中断后,判断线程的中断状态
break; //如果线程被中断过,则退出循环
}
//④线程被唤醒后重新获取锁,锁状态恢复到savedState
//不管线程是:未中断,还是signal中断,singnal后中断,前面的代码 都会保证node节点进入同步队列。
//acquireQueued 方法获取到锁,并且在获取锁park的过程中有被中断,并且之前在await过程中,不是被signal之前就中断的情况,则标记后续处理中断的情况为interruptMode。
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
//⑤重新获取到锁,把节点从condition队列中去除,同时也会清除被取消的节点
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
//⑥线程被中断,根据中断条件选择抛出异常或者重新中断传递状态
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
//向Condition队列添加等待者
private Node addConditionWaiter() {
Node t = lastWaiter;
// If lastWaiter is cancelled, clean out.
if (t != null && t.waitStatus != Node.CONDITION) { //如果队尾节点已经取消,则先清空队列中的取消节点
unlinkCancelledWaiters();
t = lastWaiter;
}
Node node = new Node(Thread.currentThread(), Node.CONDITION); //加入到Condition队列中的节点状态都为CONDITION
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
从以上代码可以看出:condition队列中,节点是通过nextWaiter来形成链表的,队列中所有节点的状态为CONDITION
//释放节点
final int fullyRelease(Node node) {
boolean failed = true;
try {
int savedState = getState(); //获取当前锁的状态
if (release(savedState)) { //全部释放掉当前状态
failed = false;
return savedState;
} else {
throw new IllegalMonitorStateException(); //释放失败抛出异常
}
} finally {
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
从fullyRelease的实现可以看出:release方法调用了tryRelease,如果tryRelease成功,则唤醒后继节点,这里要注意的是如果release不成功,则会抛出IllegalMonitorStateException异常,其实我认为这里的else里面抛出异常是多余的,因为就tryRelease的实现来看,如果不是本线程去释放自己获得的锁,tryRelease本身就会抛出IllegalMonitorStateException异常的,而如果是本线程在释放锁,那一定是在持有锁的情况下来释放锁的,这种情况一定会成功的,所以根本不会release失败,所以代码怎么都进不到else中去。但是可以总结出的是,await等方法一定是要在线程获取AQS锁的情况下调用,否则就会抛出异常。另外,如果在释放过程中线程中断,则将节点设置为CANCELLED。
③
//检查Condition队列中节点在等待过程中的中断状态
//THROW_IE:表示在signal之前被中断唤醒
// REINTERRUPT:表示在signal之后有中断,在singnal之后被通断,需要保证singnal的行为最终完成,所以中断只用延续状态状态REINTERRUPT,不用抛出异常。
/**
* Checks for interrupt, returning THROW_IE if interrupted
* before signalled, REINTERRUPT if after signalled, or
* 0 if not interrupted.
*/
private int checkInterruptWhileWaiting(Node node) {
return Thread.interrupted() ? //当前线程中断状态
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
0;
}
final boolean transferAfterCancelledWait(Node node) {
//如果节点是被signal唤醒,则状态会被更新为0,然后入同步队列,最后才是被unpark,所以这里如果能CAS成功,则说明节点没有被signal,所以线程是在await过程中被中断的。所以在这里需要将节点入队列。
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
enq(node); //节点入同步队列
return true;
}
/*
* If we lost out to a signal(), then we can't proceed
* until it finishes its enq(). Cancelling during an
* incomplete transfer is both rare and transient, so just
* spin.
*/
//如果节点是被signal唤醒的,则节点应该会在同步队列中,什么情况下被signal唤醒但是node节点不在同步队列中,而等待一会儿就在同步队列中了,这点确实没想明白。
while (!isOnSyncQueue(node))
Thread.yield();
return false;
}
8.2 signal
signal的用途是唤醒调用await方法后进入park的线程。主要代码如下:
public final void signal() {
if (!isHeldExclusively()) //判断当前线程是否是锁的持有者,如果不是则抛出异常
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null) //头节点不为空则链表不为空
doSignal(first);
}
下面详细看下doSignal的实现:
//唤醒在该条件上等待时间最长的且状态正常的节点
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) && //循环从first尝试将节点转换为同步队列节点,直到转换成功或者遍历完链表。
(first = firstWaiter) != null);
}
final boolean transferForSignal(Node node) {
/*
* If cannot change waitStatus, the node has been cancelled.
*/
//如果CAS失败,则说明节点状态不为CCONDITION,则返回false继续尝试下一个节点。
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
/*
* Splice onto queue and try to set waitStatus of predecessor to
* indicate that thread is (probably) waiting. If cancelled or
* attempt to set waitStatus fails, wake up to resync (in which
* case the waitStatus can be transiently and harmlessly wrong).
*/
Node p = enq(node); //将node节点加入同步队列
int ws = p.waitStatus;
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) //如果队列中前节点被取消或者CAS前节点状态为SIGNAL失败,手动唤醒已经进入同步队列的node节点
LockSupport.unpark(node.thread);
return true;
}
写在最后:看AQS的源码真的是花了不少的时间,但是其中任然有一些我认为不应该这样写的逻辑,我知道大部分情况是我没想到作者所思考的情况,所以还请各位看客轻拍,如果觉得对大家有帮助麻烦点个赞。
网友评论