AbstractQueuedSynchronizer
www
what
AbstractQueuedSynchronizer,简称AQS,抽象的队列同步器。位置在java.util.concurrent.locks包下。它用volatile修饰的int成员变量来表示同步状态,用虚拟的双向队列来完成线程获取资源的排队工作。它的实现基于模板方法模式,也就是使用者需要继承该类并实现其指定的方法。
where
它定义了一套多线程访问共享资源的同步框架。当前JUC下的一些锁的实现就是基于这套框架。例如:ReentrantLock,ReentrantReadWriteLock,CountDownLatch,Semaphore。
why
1.使用了CAS非阻塞算法,在一些情况下可以提升并发性能
2.提供了互斥和共享两种访问共享资源的方式
3.它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。我们只需要重写同步状态的获取和释放方法
4.提供了条件组件功能
API
1.操作同步状态相关方法
同步状态是通过CAS算法来保证其操作的原子性的。
CAS算法,全称CompareAndSwap,比较和交换算法。它是通过JNI实现的,也就是 java native interface,其允许Java调用其他语言。现代的CPU提供了一些特殊的指令,可以自动更新共享资源,而且可以检测到其他线程的干扰。CAS就用这些代替了锁。
CAS有三个操作数,内存地址V,旧的期望值A,拟写入的新值B,只有当内存地址V存储的值和A相等时,才会将B覆盖A,否则直接返回。
方法名称 | 描述 |
---|---|
int getState() | 获取同步状态state |
void setState(int newState) | 设置同步状态 |
boolean compareAndSetState(int expect, int update) | 通过CAS设置同步状态 |
CAS的缺点主要有三个:
1.一次只能操作一个共享变量,一旦有多个共享变量,就不能保证原子性。
2.ABA问题。
3.如果自旋CAS长时间未获得锁,会给CPU带来巨大的执行开销
2.需要重写的方法
AQS中有5个方法是必须要重写的,否则会抛出UnsupportedOperationException。重写方法不建议用阻塞的方法,因为等待唤醒在AQS中已经实现了,不需要我们再去对线程状态进行操作。一但我们操作失误,可能会导致没有线程去唤醒后继节点,队列中断,不再活跃。
方法名称 | 描述 |
---|---|
boolean tryAcquire(int arg) | 尝试独占式获取同步状态。成功则返回true,失败返回false。实现此方法需要查询当前状态并判断同步状态是否符合预期,然后再进行CAS设置同步状态 |
int tryAcquireShared(int arg) | 尝试共享式获取同步状态,如果返回值大于等于0,则成功,反之失败。返回0代表没有剩余资源,正数代表有剩余资源 |
boolean tryRelease(int arg) | 尝试独占式释放同步状态,成功为true,失败为false |
boolean tryReleaseShared(int arg) | 尝试共享式释放同步状态,成功为true,失败为false |
boolean isHeldExclusively() | 判断当前同步状态是否是独占式持有。一般用来判定同步状态是否被当前线程独占 |
3.主要入口方法
这几个入口方法都是public final 修饰的,可以被调用,但是禁止子类重写。
方法名称 | 描述 |
---|---|
void acquire(int arg) | 独占式获取同步状态的顶层入口,如果获取到共享资源,线程直接返回,否则进入同步队列等待,直到获取到资源为止。该方法会调用重写的tryAcquire(int arg)方法。不响应中断。 |
void acquireInterruptibly(int arg) throws InterruptedException | 功能和acquire一样,可以响应中断。 |
boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException | 功能和acquire一样,可以设置超时时间,响应中断。其中nanosTimeout的值为超时时间,传入的值必须大于0,单位为纳秒。 |
boolean release(int arg) | 独占式释放同步状态的顶层入口。它会减少指定量的同步状态,如果彻底释放了,它会唤醒同步队列中的后继线程。true代表完全释放成功,false代表释放失败或者尚未完全释放。该方法会调用重写的tryRelease(int arg)方法。 |
void acquireShared(int arg) | 共享式获取同步状态顶层入口。它会增加指定量的同步状态,成功则直接返回,失败进入同步队列,直到获取到资源为止,整个过程忽略中断。该方法会调用重写的tryAcquireShared(int arg)方法。 |
void acquireSharedInterruptibly(int arg) throws InterruptedException | 功能和acquireShared一样,此方法响应中断。 |
boolean tryAcquireSharedNanos(int arg, long nanosTimeout) throws InterruptedException | 功能和acquireShared一样,此方法可以设置超时时间,响应中断。 |
boolean releaseShared(int arg) | 共享式释放同步状态顶层入口。如果成功释放则唤醒后继线程。该方法会调用重写的tryRelease(int arg)方法。 |
源码
1.Node类
//等待队列节点类
//等待队列是“CLH”的变体锁队列
//每个节点中的status字段来跟踪线程是否需要阻塞
//队列中的每一个节点都充当特定的通知样式监视器
//要将其排队到CLH队列,可以将线程节点化拼接为新的尾节点,要出列,只要设置head字段
// +------+ prev +-----+ prev +-----+
// head | | <---- | | <---- | | tail
// | | ----> | | ----> | |
// +------+ next +-----+ next +-----+
// 插入CLH只要将节点放到尾巴上即可,出列也是,只要更新头部即可,但是需要花费一些时间来确定继承者
//prev链接主要用来取消节点。当某个节点取消了,就需要继承者重新链接到未取消的节点
//我们用next节点来实现阻塞机制。每个节点的线程ID都保存在节点中,前驱节点通过遍历来唤醒后面非取消的继承者
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;
//此等待状态只能是上面4个值和0
//0是线程构造节点时等待状态的默认值
//此状态的值需要使用CAS的方式更改
volatile int waitStatus;
//链接到当前线程/节点所依赖的前驱节点
//用于检查前驱节点的等待状态
//在取消一个前置节点时,我们需要向前迭代寻找一个非取消节点。此方法不会短路,因为头节点永远不会被取消
//一个被取消的节点永远不会变成头部
//一个线程只会取消自身,不会取消其他节点
//出列时清空,为了GC
volatile Node prev;
//链接到当前节点的后继节点
//此节点信息要在enq方法之后才会有,因此看到next节点为空的时候不一定意味着当前节点处在队列的末尾
//如果当前字段为空,我们可以通过尾部扫描prev来进行双重检查
//出列时清空,为了GC
volatile Node next;
//将此节点排入队列的线程,在构造节点时初始化,使用后设置为null
volatile Thread thread;
}
2. head,tail 和 state
head, tail,state是AQS的成员变量。有volatile修饰,也是共享变量。
volatile保证了变量在线程中的可见性。因为jvm就是通过volatile调动了缓存一致性机制。对volatile域的进行写操作时,生成的汇编指令会有一个lock前缀。该lock前缀表示jvm会向cpu发送一个信号,该信号有两个作用:1.对于该变量的改写立即刷新到主存 2.通过总线通知其他cpu该共享变量已被更新
volatile也保证了有序性。因为volatile修饰的变量禁止重排序。
//等待队列的头,延迟初始化。除了初始化时,只能通过setHead方法进行修改。
//如果head存在,它的waitStatus保证不是取消。
private transient volatile Node head;
//等待队列的尾部,延迟初始化。
//仅通过修改方法enq来添加新的等待节点。
private transient volatile Node tail;
//同步状态
//当state == 0时,代表锁没有被占据
private volatile int state;
head和tail都是node节点引用。head在逻辑上表示当前获取锁的线程,是虚节点,不存储任何信息。
head和tail都是延迟加载的,也就说只有需要的时候才会加载。如果所有线程都获取到了锁,那么head和tail都为空。
head节点和tail节点共同构造了一个虚拟的双向队列。没有队列的实体。在获取中间节点时,就从head节点向后迭代获取或者从tail节点向前迭代获取。
3.CAS方法
//如果当前状态值等于期望值,则以原子方式将同步状态设置为给定的更新值。
//如果成功,则更新新值 错误的返回值表示实际值不等于期望值。
//@param expect 期望值
//@param update 新值
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
我们是用的CAS方法省略了一些参数,我们来看下CAS相关参数源码
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;
static {
try {
stateOffset = unsafe.objectFieldOffset
(AbstractQueuedSynchronizer.class.getDeclaredField("state"));
headOffset = unsafe.objectFieldOffset
(AbstractQueuedSynchronizer.class.getDeclaredField("head"));
tailOffset = unsafe.objectFieldOffset
(AbstractQueuedSynchronizer.class.getDeclaredField("tail"));
waitStatusOffset = unsafe.objectFieldOffset
(Node.class.getDeclaredField("waitStatus"));
nextOffset = unsafe.objectFieldOffset
(Node.class.getDeclaredField("next"));
} catch (Exception ex) { throw new Error(ex); }
}
//CAS设置头部,仅仅在enq方法中使用
private final boolean compareAndSetHead(Node update) {
return unsafe.compareAndSwapObject(this, headOffset, null, update);
}
//CAS设置尾节点,仅仅在enq方法中使用
private final boolean compareAndSetTail(Node expect, Node update) {
return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}
//CAS设置节点的等待状态
private static final boolean compareAndSetWaitStatus(Node node,
int expect,
int update) {
return unsafe.compareAndSwapInt(node, waitStatusOffset,
expect, update);
}
//CAS设置节点的next节点状态
//只在cancleAcquire中使用
private static final boolean compareAndSetNext(Node node,
Node expect,
Node update) {
return unsafe.compareAndSwapObject(node, nextOffset, expect, update);
}
CAS底层都是4个参数,第一个代表要修改的内存的值所在的对象实例,第二是需要修改的的值的内存地址,第三个是期望值,第四个是要修改的新值。
4.独占式获取同步状态
顶层入口方法:acquire
//以独占模式获取,忽略中断。 通过至少调用一次{@link #tryAcquire}并成功返回来实现。
// 否则,线程将排队,并可能反复阻塞和解除阻塞,并调用{@link #tryAcquire}直到成功。
// 此方法可用于实现方法{@link Lock#lock}。 @param arg获取参数。
// 此值会传送到{@link #tryAcquire}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
上述代码主要完成了同步状态获取、节点构造、加入同步队列以及在同步队列中自旋等待的相关工作,其主要逻辑是:首先用自定义同步器实现的 tryAcquire(int arg) 方法,该方法保证线程安全的获取同步状态,如果同步状态获取失败,则构造同步节点(独占式 Node.EXCLUESIVE,同一时刻只能有一个线程成功获取同步状态)并通过 addWaiter(Node node) 方法将该节点加入到同步队列的尾部,最后调用 acquireQueued(Node node, int arg) 方法,使得该节点以死循环的方式获取同步状态。如果获取不到则阻塞节点中的线程,而被阻塞线程的唤醒主要依靠前驱节点的出队或阻塞线程被中断来实现。
简单来说:
1.tryAcquire() 尝试直接去获取锁,如果成功则直接返回
2.addWaiter(Node.EXCLUSIVE) 将该线程封装成节点加入到同步等待队列的尾部,并标记为独占模式。
3.acquireQueued() 使线程在同步等待队列中获取资源,一直获取到资源才返回。如果在整个等待过程被中断过,就返回true,否则返回false
4.selfInterrupt() 如果线程在等待过程中被中断过,中途是不响应的。等获取到资源后补上中断标记
我们来具体看下相关方法的源码
//尝试以独占模式进行获取。此方法应查询对象的状态是否允许以独占模式获取它,如果允许则获取它。
// <p>此方法始终由执行获取的线程调用。如果此方法返回false,则acquire方法可以让线程排队(如果尚未排队),直到其他某个线程释放锁为止。
//默认实现抛出{@link UnsupportedOperationException}。
//@param arg获取参数。该值始终是传递给获取方法的值,或者是在条件等待输入时保存的值。
// @return {@code true}如果成功。成功后,便已获取此对象。
//@throws IllegalMonitorStateException如果获取会使该同步器处于非法状态。必须以一致的方式抛出此异常,以使同步正常工作。
//@throws UnsupportedOperationException如果不支持独占模式
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
上述代码通过使用 compareAndSetTail(Node expect, Node update) 方法来确保节点能够被线程安全添加。
//为当前线程和给定模式创建节点并排队
//Node.EXCLUSIVE表示独占,Node.SHARED表示共享
//返回新节点
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// 快速尝试一次,如果失败就进入enq方法循环尝试
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
在上述方法中,同步器通过死循环来保证节点的正确添加,在死循环中,只有通过CAS将节点设置为尾节点之后,当前线程才能从该方法返回,否则当前线程需要不断地尝试设置。可以看出,enq(final Node node) 方法将并发添加节点的请求通过CAS变得 串行化了。
//将节点插入队列,必要时进行初始化。
// @param插入节点
//@return插入节点的前驱节点
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;
}
}
}
}
//在队列中线程用独占不间断的模式去请求锁。
// @param节点节点@param arg获取参数
//@return {@code true}如果在等待时被中断
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);
}
}
//将队列头设置为节点,从而出队。 仅通过acquire方法调用。
// 出于GC和抑制不必要的信号和遍历的目的,还应清空未使用的字段。
//@param节点节点
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
从这里可以看到,头节点是虚节点的原。一旦节点变成头节点之后,是会将线程信息设置为null,这样可以减少内存的消耗。
//检查并更新无法获取的节点的状态。 如果线程应阻塞,则返回true。
// 在请求锁的循环中,此方法主要用来控制等待状态。
//请求中的pred == node.prev。
//@param pred 前驱节点
//@param node 节点
//如果线程应该阻塞,则节点@return {@code true}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
//该节点已经设置了状态,前驱释放锁的时候会通知自己,因此可以安全的阻塞。
return true;
if (ws > 0) {
//前驱节点已取消。 跳过前驱节点并重新指定新的前驱节点。
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//waitStatus必须为0或PROPAGATE。 表示我们需要一个信号,但是此次不会阻塞。
//请求线程将需要重试以确保在阻塞之前无法获取。
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
此方法只是用于控制前驱节点的状态。
//堵塞的便捷方法,然后检查是否中断
//@return {@code true}如果被中断
//interrupted会清除中断标记,因此tryAcquire方法如果线程被中断过,需要走selfInterrupt,补一个中断标记
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
park()方法会让线程进入waiting状态。在此状态下,有两种途径可以唤醒线程。1.被unpark() 2.被interrupt()。
//取消正在进行的尝试获取
private void cancelAcquire(Node node) {
if (node == null)
return;
node.thread = null;
// 遍历并更新节点前驱,把node的prev指向前部第一个非取消节点。
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
Node predNext = pred.next;
// 直接把当前节点的等待状态置为取消,后继节点即便也在cancel可以跨越node节点。
node.waitStatus = Node.CANCELLED;
// 如果我们是尾节点,那我们将移除自身
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
//如果后继节点需要被唤醒,就将前驱节点和后继节点连起来
// 如果前驱节点状态不是signal,那就设置为signal来保证队列的活跃性
int ws;
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
Node next = node.next;
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
/*
* 这时说明pred == head或者pred状态取消或者pred.thread == null
* 在这些情况下为了保证队列的活跃性,需要去唤醒一次后继线程。
* 举例来说pred == head完全有可能实际上目前已经没有线程持有锁了,
* 自然就不会有释放锁唤醒后继的动作。如果不唤醒后继,队列就挂掉了。
*
* 这种情况下看似由于没有更新pred的next的操作,队列中可能会留有一大把的取消节点。
* 实际上不要紧,因为后继线程唤醒之后会走一次试获取锁的过程,
* 失败的话会走到shouldParkAfterFailedAcquire的逻辑。
* 那里面的if中有处理前驱节点如果为取消则维护pred/next,踢掉这些取消节点的逻辑。
*/
unparkSuccessor(node);
}
/*
* 取消节点的next之所以设置为自己本身而不是null,
* 是为了方便AQS中Condition部分的isOnSyncQueue方法,
* 判断一个原先属于条件队列的节点是否转移到了同步队列。
*
* 因为同步队列中会用到节点的next域,取消节点的next也有值的话,
* 可以断言next域有值的节点一定在同步队列上。
*
* 在GC层面,和设置为null具有相同的效果。
*/
node.next = node; // help GC
}
}
//唤醒后继线程
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
// 尝试将node的等待状态置为0,这样的话,后继争用线程可以有机会再尝试获取一次锁。
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
/*
* 这里的逻辑就是如果node.next存在并且状态不为取消,则直接唤醒s即可
* 否则需要从tail开始向前找到node之后最近的非取消节点。
*
* 这里为什么要从tail开始向前查找也是值得琢磨的:
* 如果读到s == null,不代表node就为tail,参考addWaiter以及enq函数中的我的注释。
* 不妨考虑到如下场景:
* 1. node某时刻为tail
* 2. 有新线程通过addWaiter中的if分支或者enq方法添加自己
* 3. compareAndSetTail成功
* 4. 此时这里的Node s = node.next读出来s == null,但事实上node已经不是tail,它有后继了!
*/
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);
}
5.可中断的独占式获取同步状态
此方法和acquire基本类似,就多了响应中断命令,就放一下抛出中断异常的地方,其他不再赘述。
顶层入口方法:acquireInterruptibly
//以独占模式获取,如果中断则中止。
//通过首先检查中断状态,然后至少调用一次{@link #tryAcquire}来实现,并成功返回。
// 此方法可用于实现方法{@link Lock#lockInterruptible}。
// @throws InterruptedException如果当前线程被中断
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
6.可超时的独占式获取同步状态
超时获取同步状态可以被视作响应中断获取同步状态过程的增强版,doAcquireNanos(int arg, long nanosTimeout)方法在支持响应中断的基础上,增加了超时获取的特性。针对超时获取,主要需要计算出需要阻塞的时间间隔nanosTimeout,为了防止过早通知,nanosTimeout计算公式为:nanosTimeout -= now - lastTime,其中now为当前唤醒时间,lastTime为上次唤醒时间,如果nanosTimeout大于0则表示超时时间未到,需要继续阻塞。nanosTimeout单位为纳秒。
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
final long deadline = System.nanoTime() + nanosTimeout;
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
nanosTimeout = deadline - System.nanoTime();
if (nanosTimeout <= 0L)
return false;
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
该方法在自旋过程中,当节点的前驱节点为头节点时尝试获取同步状态,如果获取成功则从该方法返回,这个过程和acquire方法类似,但是在同步状态获取失败的处理上有所不同。如果当前线程获取同步状态失败,则判断是否超时(nanosTimeout小于等于0表示已经超时),如果没有超时,则重新计算超时间隔nanosTimeout,然后使当前线程等待nanosTimeout纳秒,当已到设置的超时时间,该线程会从LockSupport.parkNanos(Object blocker, long nanos)方法返回。
如果nanosTimeout小于等于spinForTimeoutThreshold(1000纳秒)时,将不会使线程进行超时阻塞,而是进入快速的自旋过程。原因在于,非常短的超时阻塞无法做到十分精确,如果这时再进行超时阻塞,反而会让nanosTimeout的超时从整体上表现得不精确。因此,在超时非常短的场景下,同步器会进入无条件的快速自旋。
6.独占式释放同步状态
顶层入口方法时release(int arg)
//tryRelease必须是完全释放才能返回true
public final boolean release(int arg) {
if (tryRelease(arg)) {
/*
* 此时的head节点可能有3种情况:
* 1. null (AQS的head延迟初始化+无竞争的情况)
* 2. 当前线程在获取锁时new出来的节点通过setHead设置的
* 3. 由于通过tryRelease已经完全释放掉了独占锁,有新的节点在acquireQueued中获取到了独占锁,并设置了head
* 第三种情况可以再分为两种情况:
* (一)时刻1:线程A通过acquireQueued,持锁成功,set了head
* 时刻2:线程B通过tryAcquire试图获取独占锁失败失败,进入acquiredQueued
* 时刻3:线程A通过tryRelease释放了独占锁
* 时刻4:线程B通过acquireQueued中的tryAcquire获取到了独占锁并调用setHead
* 时刻5:线程A读到了此时的head实际上是线程B对应的node
* (二)时刻1:线程A通过tryAcquire直接持锁成功,head为null
* 时刻2:线程B通过tryAcquire试图获取独占锁失败失败,入队过程中初始化了head,进入acquiredQueued
* 时刻3:线程A通过tryRelease释放了独占锁,此时线程B还未开始tryAcquire
* 时刻4:线程A读到了此时的head实际上是线程B初始化出来的傀儡head
*/
Node h = head;
// head节点状态不会是CANCELLED,所以这里h.waitStatus != 0相当于h.waitStatus < 0
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
unparkSuccessor方法已经在上面看过了,释放方法很简单,主要就是释放锁之后需要唤醒后继线程。
7.共享式获取同步状态
共享式获取和独占式获取最主要的区别在于同一时刻能否有多个线程同步获取到同步状态。
顶层入口方法是acquireShared(int arg)
public final void acquireShared(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);
//独占式是判定true或者false
if (r >= 0) {
//独占式是setHead方法
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);
}
}
//设置队列头,并检查后继者是否可能在共享模式下等待
//如果propagate >0,表示需要将唤醒传播下去
//@param propagate tryAcquireShared的返回值
private void setHeadAndPropagate(Node node, int propagate) {
// 把当前的head封闭在方法栈上,用以下面的条件检查
Node h = head;
setHead(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();
}
}
//共享模式下的释放同步状态-通知后继节点并确保传播。
//(注意:对于独占模式,如果需要通知,此方法仅相当于unparkSuccessor。)
private void doReleaseShared() {
//即使有其他正在进行的获取/释放,也要确保发布传播。
// 如果需要通知,将以尝试unparkSuccessor的常规方式进行。
//但是,如果没有,则将状态设置为PROPAGATE,以确保释放后继续传播。
// 此外,在执行此操作时,必须循环以防添加新节点。
// 另外,与unparkSuccessor的其他用法不同,我们需要知道CAS重置状态是否失败,如果失败则继续循环。
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
//如果节点状态是signal,后继线程就会阻塞,所以改成0,不让后继阻塞
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // 循环检查状态
unparkSuccessor(h);
}
// 如果h节点的状态为0,需要设置为PROPAGATE用以保证唤醒的传播。
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
if (h == head) // loop if head changed
break;
}
}
共享式和独占式的区别主要在于
1.获取同步状态方法的返回值不同
2.唤醒后继线程的条件不同
8.共享式释放同步状态
releaseShared(int arg)方法是共享式释放同步状态的顶层入口。
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
// doReleaseShared的实现上面获取共享锁已经介绍
doReleaseShared();
return true;
}
return false;
}
共享式同步状态的获取和释放共享了doReleaseShared方法,用于实现唤醒的传播。