目录
一、前言
Java同步队列器(AbstractQueuedSynchronizer)是由Doug Lea大师完成,是用来构建锁或者其他同步组件的基础框架,它使用一个volatile修饰的int成员变量来表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。
同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态,当子类在实现抽象方法时需要访问或修改同步状态时,同步队列器提供了一下三个方法来进行操作:
- getState():获取当前同步状态
- setState():设置当前同步状态
- compareAndSetState(int expect, int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性
同步器的设计基于模板方法模式,使用者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而同步器中的模板方法将会调用使用者重写的方法,在实现自定义同步组件时,同步队列器的子类被推荐定义为自定义同步组件的静态内部类,在ReentrantReadWriteLock的设计中体现了这一思想,ReentrantReadWriteLock本身并未继承AbstractQueuedSynchronizer,而是通过静态内部类Sync来重写同步器的指定方法:
public class ReentrantReadWriteLock
implements ReadWriteLock, java.io.Serializable {
abstract static class Sync extends AbstractQueuedSynchronizer {
}
static final class NonfairSync extends Sync {
}
static final class FairSync extends Sync {
}
}
那么同步器的子类可以重写哪些方法呢?如下表所示:
方法名称 | 描述 |
---|---|
protected boolean tryAcquire(int arg) | 独占式获取同步状态,实现该方法需要查询当前状态并判断同步状态是否符合预期,然后进行CAS设置同步状态 |
protected boolean tryRelease(int arg) | 独占式释放同步锁,等待获取同步状态的线程将有机会获取到同步状态 |
protected int tryAcquireShared(int arg) | 共享式获取同步状态,返回大于等于0的值,表示获取成功,反之,获取失败 |
protected boolean tryReleaseShared(int arg) | 共享式释放同步状态 |
protected boolean isHeldExclusively() | 当前线程是否在独占模式下被线程占用,一般该方法是否被当前线程所独占 |
同步器提供的模板方法有如下:
方法名称 | 描述 |
---|---|
void acquire(int arg) | 独占式获取同步状态,如果当前线程获取同步状态成功,则由该方法返回,否则,将会进入同步队列等待,该方法将会调用重写的tryAcquire(int arg)方法 |
void acquireInterruptibly(int arg) | 与acquire(int arg)相同,但是该方法响应中断,当前线程未获取到同步状态进入同步队列中,如果当前线程被中断,则该方法会抛出InterruptedException并返回 |
boolean tryAcquireNanos(int arg, long nanos) | 在acquireInterruptibly(int arg)基础上增加了超时限制,如果当前线程在超时时间内没有获取到同步状态,那么将会返回false,如果获取到了返回true |
void acquireShared(int arg) | 共享式的获取同步状态,如果当前线程为获取到同步状态,将会进入同步队列等待,与独占式获取的主要区别是在同一时刻可以有多个线程获取同步状态 |
void acquireSharedInterruptibly(int arg) | 与acquireShared(int arg)相同,该方法响应中断 |
boolean tryAcquireSharedNanos(int arg, long nanos) | 在acquireSharedInterruptibly(int arg)基础上增加了超时限制 |
boolean release(int arg) | 独占式的释放同步状态,该方法会在释放同步状态后,将同步队列中第一个节点包含的线程唤醒 |
boolean releaseShared(int arg) | 共享式的释放同步状态 |
Collection<Thread> getQueueThreads() | 获取等待在同步队列上的线程集合 |
同步器提供的模板方法基本上分为3类:独占式获取与释放同步状态、共享式获取与释放同步状态和查询同步队列中等待线程的情况。
JDK实现的使用AQS的自定义同步有:
类图关系如下:
二、同步队列
同步器依赖内部的同步队列(一个FIFO双向队列)来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态
同步队列中的节点(Node)用来保存获取同步状态失败的线程引用、等待状态以及前驱和后继节点,Node在同步器中以静态内部类的形式定义:
static final class Node {
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
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() {
}
Node(Thread thread, Node mode) {
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) {
this.waitStatus = waitStatus;
this.thread = thread;
}
}
Node节点主要有如下几个属性:
volatile int waitStatus:该变量用来表征节点所处的等待状态,可分为以下几种:
- CANCLLED:值为1,由于在同步队列中等待的线程等待超时或者中断,需要从同步队列中取消等待,节点进入该状态将不再变化
- SIGNAL:值为-1,节点的后继节点的线程处于等待状态,而当前节点的线程如果释放了同步状态或者被取消,当前节点处于SIGNAL状态将会通知后继节点,使后继节点的线程得以运行
- CONDITION:值为-2,该状态表示节点处于等待队列中,线程调用wait()方法处于阻塞状态,等待Condition条件,当其他线程对Condition调用了signal()方法后,该节点将会从等待队列中转移到同步队列中,加入到对同步状态的获取中
- PROPAGATE:值为-3,与共享模式有关,处于该状态的节点在共享模式下处于可运行状态,并且该节点获取同步状态后会无条件的向下一个节点传播下去
- INITIAL:值为0,初始状态
volatile Node prev:前驱节点,当节点加入同步队列时被设置
volatile Node next:当前节点的后继节点
volatile Thread thread:当前节点获取同步状态的线程
Node nextWaiter:Node既可以作为同步队列节点使用,也可以作为Condition的等待队列节点使用。在作为同步队列节点时,nextWaiter可能有两个值:EXCLUSIVE、SHARED标识当前节点是独占模式还是共享模式;在作为等待队列节点使用时,nextWaiter保存后继节点
AQS定义了同步队列的队列头、队列尾和一个int类型的共享资源:
// 同步队列队列头
private transient volatile Node head;
// 同步队列队列尾
private transient volatile Node tail;
// 标识共享资源
private volatile int state;
没有获取到同步状态的线程会构造一个节点加入到队列尾部,同步队列的基本结构如下所示:
同步器包含两个节点类型的引用,一个指向头结点,一个指向尾节点,在同一个时刻,可能存在多个线程请求同步状态,当有一个线程获取到同步状态时,其他线程将构造成节点加入到同步队列,在对同步队列进行操作时,必须保证线程安全,因此同步器提供了基于CAS的设置尾节点的方法compareAndSetTail(Node expect, Node update):
private final boolean compareAndSetTail(Node expect, Node update) {
return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}
同步器将节点加入到同步队列的过程如下所示:
同步节点遵循FIFO,首节点是获取同步状态成功的节点,首节点的线程在释放同步状态时,将会唤醒后继节点,而后继节点将会在获取同步状态成功时将自己设置为首节点:
在设置首节点时,因为只有获取到同步状态的线程才会将自己设置为首节点,又由于只有一个线程能够获取到同步状态,因此设置首节点的操作只能由一个线程来完成,不需要CAS操作,只需要将首节点设置为原首节点的后继节点(即获取到同步状态的线程节点),并断开原首节点的next引用。
三、独占式同步状态
1、获取同步状态
同步器提供了模板方法acquire(int arg)来获取同步状态,acquire(int arg)方法代码如下:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 如果线程是被中断而被唤醒的,重置中断状态
selfInterrupt();
}
该方法首先会调用子类重写的tryAcquire(int arg)方法来获取同步状态,当获取失败时,会调用addWaiter(Node mode)方法构造同步节点,并将节点加入到同步队列的队尾,最后调用acquireQueued(final Node node, int arg)方法自旋的获取同步状态。
addWaiter(Node mode)源代码如下:
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
首先构造一个独占式Node,同步pred != null判断同步队列是否为空,如果不为空,则通过compareAndSetTail(Node expect, Node update)将节点线程安全的添加到队尾,如果pred == null或CAS添加失败,则调用enq(final Node 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;
}
}
}
}
通过源码可以看到,enq函数主要为一个for循环,enq函数首先判断队列是否为空,如果队列为空,则构造一个空节点,并将队列的首节点和尾节点设置为该空节点,这时同步队列将不再为空,进入else语句,通过CAS设置同步队列的队尾为新添加的节点,对于队首的空节点,由于不用去竞争同步状态,因此不影响后面的线程获取同步状态。for循环中的任何一步CAS操作失败,则将不断的在for循环里重试,直到成功为止,可以看出,enq(final Node node)函数将并发添加节点的请求通过CAS变得“串行化”了。
注:为何JDK原理的死循环都是for(;;)而不是while(1),因为while(1)编译之后是mov eax,1 test eax,eax je foo+23h jmp foo+18h,for(;;)编译之后是jmp foo+23h,可以看出for(;;)指令少,不占用寄存器,没有判断跳转,效率更高
当同步节点成功添加进同步队列后,会调用acquireQueued(final Node node, int arg)方法进入一个自旋的过程,每个节点(每个节点对应一个线程)都在自省地观察,当条件满足,获取到同步状态,就可以从自旋的过程中退出,否则依旧留在自旋这个过程中(并会阻塞节点的线程),acquireQueued(final Node node, int arg)源码如下:
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())
// 说明当前线程是被中断唤醒的。
// 除非碰巧线程中断后acquire成功了,那么根据Java的最佳实践,
// 需要重新设置线程的中断状态(acquire.selfInterrupt)
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
在for循环获取同步状态的过程中,只有节点的前驱节点是头结点是才能够尝试获取同步状态,原因有一下两点:
- 头节点是成功获取同步状态的节点,而头节点的线程释放了同步状态之后,将会唤醒器后继节点,后继节点的线程被唤醒之后需要检查自己的前驱节点是否是头结点
- 维护同步队列的FIFO原则,在该方法中,节点自旋获取同步状态的行为如下所示:
如上图所示,由于非首节点前驱节点出队(在函数shouldParkAfterFailedAcquire(Node pred, Node node)中,会将CANCELED状态的节点出队),或是非首节点线程被中断而从等待状态返回,随后检查自己的前驱节点是否为头节点,如果是则尝试获取同步状态。可以看到节点和节点之间在循环检查的过程中基本不进行通信,而是简单地判断自己的前驱是不是头节点,这样就使节点的释放规则符合FIFO,并且也便于对过早通知的处理(过早通知是指前驱节点不是头节点的线程由于中断而被唤醒)。
shouldParkAfterFailedAcquire(Node pred, Node node)函数的源码如下所示:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
return true;
// 过滤掉所有ws > 0的前驱节点,即CANCELLED = 1的节点
// 节点在初始化时waitStatus值为0
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 将前驱结点的状态设置为Node.SIGNAL,表示其后继结点需要被唤醒
// 0 或 PROPAGATE (CONDITION用在ConditonObject,这里不会是这个值)
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
// 返回为false让线程先不要阻塞
// 再次判断前驱节点是否为首节点,并获取同步状态,如果获取失败,才阻塞线程
return false;
}
在shouldParkAfterFailedAcquire(Node pred, Node node)函数中,主要做了两件事:
- 过滤掉前驱节点中处于CANCLLED状态的节点,并将CANCLLED状态的节点出队
- 将前驱节点的状态设置为SIGNAL,表示释放同步状态时需要通知后继节点
最后调用parkAndCheckInterrupt()函数阻塞线程知道被前驱节点唤醒或是被中断:
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
public class LockSupport {
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, 0L);
setBlocker(t, null);
}
private static void setBlocker(Thread t, Object arg) {
// Even though volatile, hotspot doesn't need a write barrier here.
UNSAFE.putObject(t, parkBlockerOffset, arg);
}
}
整个获取同步状态的流程如下所示:
2、释放同步状态
当线程获取到了同步状态并执行了相应逻辑之后,需要释放同步状态,并且使后续节点能够获取同步状态。通过调用同步器的release(int 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;
}
在unparkSuccessor(Node node)方法中,会唤醒首节点的后继节点,当后继节点为null或是被取消,则会从tail节点向前遍历,找到需要唤醒的节点,源码如下:
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
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);
}
为什么需要从tail节点往前遍历来找到需要唤醒的节点呢:
- 因为若正向寻找,当前头节点的后继节点可能出现变为空的情况,如在setHeadAndPropagate方法执行后会将老的头节点的后继节点置为空,以告诉GC实现回收,这样的话后继节点找不到,则将无法找到状态不大于0的后续节点,即使该节点是存在的。而从尾节点倒序寻找将确保能够找到该目标节点(前提是存在),因为尾节点的设置是线程安全的。
- 在入队列的过程enq(final Node node)方法中,if(compareAndSetTail(t, node))和t.next = node这两行代码中间pred的next指针是为空的,而且如果unparkSuccessor方法从头部向后遍历中,判断某个节点的next指针是否为空的逻辑恰好在这两行代码之间,而某个节点恰好又是pred节点,所以就找不到真正需要unpark的节点,所以就导致了死锁,也就是后续节点永远不可能被唤醒。所以必须要从尾巴向前进行遍历,找到真正需要unpark的节点。
- 在取消节点的方法cancelAcquire(Node node)中,cancelAcquire只是设置了next的变化,没有设置prev的变化,在最后有这样一行代码:node.next = node,如果这时执行了unparkSuccessor方法,并且向后遍历的话,就成了死循环了,所以这时只有prev是稳定的。
三、共享式同步状态
共享式同步状态与独占式同步状态最主要的区别在于同一时刻能否有多个线程同时获取同步状态。
1、共享式同步状态获取
通过调用同步器的acquireShared(int arg)方法可以共享式地获取同步状态,该方法源码如下:
public final void acquireShared(int arg) {
// 首先尝试获取同步状态
// 如果获取失败,则调用doAcquireShared(int arg)自旋获取同步状态
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
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);
// 判断是否获取锁成功,获取成功则设置新的头节点,并唤醒后继节点
if (r >= 0) {
// 设置头节点并决定是否唤醒后继节点
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
在acquireShared(int arg)方法中,同步器调用tryAcquireShared(int arg)方法尝试获取同步状态,tryAcquireShared(int arg)方法返回值为int类型,当返回值大于等于0时,表示能够获取同步状态。因此,在共享式获取自旋过程中,成功获取同步状态并退出自旋的条件就是tryAcquireShared(int arg)方法返回值大于等于0。可以看到,在doAcquireShared(int arg)方法的自旋过程中,如果当前节点的前驱节点为头节点时,尝试获取同步状态,如果返回值大于等于0,表示该次获取同步状态成功并从自旋过程中退出。
注意:
当tryAcquireShared(int arg)返回值为0时,表示没有剩余的共享资源,后续节点不能够获取到同步状态,只有返回值大于0后续节点才可以获取到同步状态。
setHeadAndPropagate(Node node, int propagate)函数主要做了两件事:
- 在获取共享锁后,设置head节点
- 根据propagate判断是否需要唤醒后继节点
源码如下:
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head;
// 设置头节点
setHead(node);
// propagate是tryAcquireShared的返回值,这是决定是否传播唤醒的依据之一
// h.waitStatus为SIGNAL或者PROPAGATE时也根据node的下一个节点共享来决定是否传播唤醒
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
在决定是否传播唤醒时,为什么不只使用propagate > 0来决定是否环境呢?
参考博文:https://www.cnblogs.com/micrari/p/6937995.html
2、共享式同步状态释放
共享式同步状态的释放
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
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))
continue;
unparkSuccessor(h);
}
else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
if (h == head)
break;
}
}