前言
Object管理monitor的方法,功能比较有限,自JDK1.5后,出现了Condition,可以实现更多的功能。同时Java里的锁,最基本的实现都用到了同步队列,在JDK里这个关键的类是AbstractQueuedSynchronizer。此篇文章主要讲述Condition框架以及AbstractQueuedSynchronizer(AQS)。
Condition
Condition是用于实现等待/通知的接口,比Object类的wait / notify 具备更丰富的功能。Condition必须与锁配合使用,一个Condition与一个Lock绑定,但一个锁可以创建多个Condition个,即创建多个monitor。好处是可以唤醒指定线程,而notify只能随机唤醒一个。
一般由Lock的newCondition方法创建:(以ReentrantLock源码为例)
final ConditionObject newCondition() {
return new ConditionObject();
}
使用Condition构建的生产者-消费者模型(同样是用长度有限的ArrayList来模拟):
public class Test2 {
public List<Integer> list = new ArrayList<>();
public static final int MAX = 5;
public int size = 0;
public Lock lock = new ReentrantLock();
public Condition consumer = lock.newCondition();
public Condition producer = lock.newCondition();
public void add(int i) {
lock.lock();
try {
if (size >= MAX) {
consumer.await();
}
list.add(i);
size++;
producer.signal();
System.out.println(Thread.currentThread().getName() + " After add " + list);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void delete(int i) {
lock.lock();
try {
if (size == 0) {
producer.await();
}
list.remove(i);
size--;
consumer.signal();
System.out.println(Thread.currentThread().getName() + "After delete " + list);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
Test2 t = new Test2();
for (int i = 0; i < 5; i++) {
Thread thread1 = new Thread(() -> {
while (true) {
t.add(1);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread1.start();
}
for (int i = 0; i < 3; i++) {
Thread thread2 = new Thread(() -> {
while (true) {
t.delete(0);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread2.start();
}
}
}
Question:Object的monitor方法与condition有何区别?
Ans:Object只支持一个等待队列,而Condition支持多个(创建多个Condition对象)。Condition还支持中断线程,设置等待时间等功能。
newCondition方法里的ConditionObject就是Condition接口的实现类,同时它还是AbstractQueuedSynchronizer的一个内部类(AQS)。因为锁的实现原理就依赖AQS,AQS内部维护了一个同步队列,如果是独占锁,所有获取锁失败的线程都会加入到同步队列中。(阻塞的线程肯定要等待,所以有一个存储它们的结构,这个结构就是AQS,不仅存储,还要同步
AbstractQueuedSynchronizer
概念
AQS是用来构建锁和其他同步组件的基本框架,通过一个原子int和FIFO队列构建的等待队列。底层通过CAS实现原子性,利用队列实现锁竞争。
AQS是实现锁或其他同步组件的关键,ReentrantLock,ThreadPoolExecutor,Semaphore以及Condition都要用到。
AQS与锁的关系:锁是面向使用者,定义了使用者和锁交互的接口,隐藏了实现细节。而同步器是面向锁的实现者,它简化了锁的实现方式,屏蔽了同步状态的管理,线程的派对,等待和唤醒等底层操作。
这5个方法,AQS里没有实现逻辑,但它的其他常用方法却会调用这几个方法。所以子类如果需要,那么就要去对这些方法进行重写。(模板方法设计模式 )
AQS方法总结:
①独占式的获取与释放同步状态
②共享式的获取与释放同步状态
③查询同步队列中等待线程的情况
例子
JDK上的例子:
public class Mutex implements Lock, java.io.Serializable {
// Our internal helper class
// 继承AQS的静态内存类
// 重写方法
private static class Sync extends AbstractQueuedSynchronizer {
// Reports whether in locked state
protected boolean isHeldExclusively() {
return getState() == 1;
}
// Acquires the lock if state is zero
public boolean tryAcquire(int acquires) {
assert acquires == 1; // Otherwise unused
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
// Releases the lock by setting state to zero
protected boolean tryRelease(int releases) {
assert releases == 1; // Otherwise unused
if (getState() == 0) throw new IllegalMonitorStateException();
setExclusiveOwnerThread(null);
setState(0);
return true;
}
// Provides a Condition
Condition newCondition() {
return new ConditionObject();
}
// Deserializes properly
private void readObject(ObjectInputStream s)
throws IOException, ClassNotFoundException {
s.defaultReadObject();
setState(0); // reset to unlocked state
}
}
// The sync object does all the hard work. We just forward to it.
private final Sync sync = new Sync();
//使用同步器的模板方法实现自己的同步语义
public void lock() {
sync.acquire(1);
}
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
public void unlock() {
sync.release(1);
}
public Condition newCondition() {
return sync.newCondition();
}
public boolean isLocked() {
return sync.isHeldExclusively();
}
public boolean hasQueuedThreads() {
return sync.hasQueuedThreads();
}
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
private static Mutex mutex = new Mutex();
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(() -> {
mutex.lock();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
mutex.unlock();
}
});
thread.start();
}
}
}
执行结果:
Mutex是Lock的一个实现类(同ReentrantLock),提供了lock,unlock,tryLock等方法。但其内部类都是调用了AQS的模板方法。这里只重写了独占锁的实现tryAcquire和tryRelease,调试时确实也只允许一个线程在运行。
对于同步组件Lock,专注于同步状态的判断,实现同步语义(如上面的tryRelease)。而对于AQS,只需要同步组件返回的true / false,根据返回值会有不同的操作。
AQS原理
①同步队列。当共享资源被某个线程占有,其他请求该资源的线程将会阻塞,从而进入同步队列。AQS的同步队列使用的是链式方法去实现,而且是一个双向队列。
AQS里有一个静态内部类Node:
/**
* Wait queue node class.
*
* <p>The wait queue is a variant of a "CLH" (Craig, Landin, and
* Hagersten) lock queue. CLH locks are normally used for
* spinlocks. We instead use them for blocking synchronizers, but
* use the same basic tactic of holding some of the control
* information about a thread in the predecessor of its node. A
* "status" field in each node keeps track of whether a thread
* should block. A node is signalled when its predecessor
* releases. Each node of the queue otherwise serves as a
* specific-notification-style monitor holding a single waiting
* thread. The status field does NOT control whether threads are
* granted locks etc though. A thread may try to acquire if it is
* first in the queue. But being first does not guarantee success;
* it only gives the right to contend. So the currently released
* contender thread may need to rewait.
*/
static final class Node {
/** Marker to indicate a node is waiting in shared mode */
static final Node SHARED = new Node();
/** Marker to indicate a node is waiting in exclusive mode */
static final Node EXCLUSIVE = null;
/** waitStatus value to indicate thread has cancelled */
static final int CANCELLED = 1;
/** waitStatus value to indicate successor's thread needs unparking */
static final int SIGNAL = -1;
/** waitStatus value to indicate thread is waiting on condition */
static final int CONDITION = -2;
/**
* waitStatus value to indicate the next acquireShared should
* unconditionally propagate
*/
static final int PROPAGATE = -3;
/**
* Status field, taking on only the values:
* SIGNAL: The successor of this node is (or will soon be)
* blocked (via park), so the current node must
* unpark its successor when it releases or
* cancels. To avoid races, acquire methods must
* first indicate they need a signal,
* then retry the atomic acquire, and then,
* on failure, block.
* CANCELLED: This node is cancelled due to timeout or interrupt.
* Nodes never leave this state. In particular,
* a thread with cancelled node never again blocks.
* CONDITION: This node is currently on a condition queue.
* It will not be used as a sync queue node
* until transferred, at which time the status
* will be set to 0. (Use of this value here has
* nothing to do with the other uses of the
* field, but simplifies mechanics.)
* PROPAGATE: A releaseShared should be propagated to other
* nodes. This is set (for head node only) in
* doReleaseShared to ensure propagation
* continues, even if other operations have
* since intervened.
* 0: None of the above
*
* The values are arranged numerically to simplify use.
* Non-negative values mean that a node doesn't need to
* signal. So, most code doesn't need to check for particular
* values, just for sign.
*
* The field is initialized to 0 for normal sync nodes, and
* CONDITION for condition nodes. It is modified using CAS
* (or when possible, unconditional volatile writes).
*/
volatile int waitStatus;
/**
* Link to predecessor node that current node/thread relies on
* for checking waitStatus. Assigned during enqueuing, and nulled
* out (for sake of GC) only upon dequeuing. Also, upon
* cancellation of a predecessor, we short-circuit while
* finding a non-cancelled one, which will always exist
* because the head node is never cancelled: A node becomes
* head only as a result of successful acquire. A
* cancelled thread never succeeds in acquiring, and a thread only
* cancels itself, not any other node.
*/
volatile Node prev;
/**
* Link to the successor node that the current node/thread
* unparks upon release. Assigned during enqueuing, adjusted
* when bypassing cancelled predecessors, and nulled out (for
* sake of GC) when dequeued. The enq operation does not
* assign next field of a predecessor until after attachment,
* so seeing a null next field does not necessarily mean that
* node is at end of queue. However, if a next field appears
* to be null, we can scan prev's from the tail to
* double-check. The next field of cancelled nodes is set to
* point to the node itself instead of null, to make life
* easier for isOnSyncQueue.
*/
volatile Node next;
/**
* The thread that enqueued this node. Initialized on
* construction and nulled out after use.
*/
volatile Thread thread;
/**
* Link to next node waiting on condition, or the special
* value SHARED. Because condition queues are accessed only
* when holding in exclusive mode, we just need a simple
* linked queue to hold nodes while they are waiting on
* conditions. They are then transferred to the queue to
* re-acquire. And because conditions can only be exclusive,
* we save a field by using special value to indicate shared
* mode.
*/
Node nextWaiter;
}
waitStatus:节点的状态
prev:当前节点 / 线程的前驱节点
next:后继节点
thread;加入到同步队列的线程引用
nextWaiter:等待队列中的下一个节点
节点的状态:
CANCELLED:节点从等待队列中取消
SIGNAL:后继节点处于等待状态,如果当前节点释放同步状态,将会通知后继节点,使得后继节点的线程能够运行
CONDITION:当前节点处于等待队列,节点在Condition里等待。当其他线程对此Condition调用singnal方法,Condition状态的节点将从等待队列转移到同步队列中,等待获取同步锁。
PROPAGATE:表示下一次共享式同步状态获取将会无条件传播下去。用于共享模式,此时前继节点不仅会唤醒后继节点,还可能会唤醒后继节点的后继节点(传播)。
INITIAL,值为0
SHARED / EXCLUSIVE : 共享锁 / 独占锁
CLH锁
知道了Node的成员变量之后,再看类前的注释,可以知道,Node使用的是一种名为CLH锁的东西,一般用于改进自旋锁.
Question:什么是CLH锁,为什么要有CLH锁,CLH锁为什么要使用双向队列?
Ans:
预备知识:CPU有三种架构,SMP,NUMA,MPP
SMP:Symmetric Multi Processing,即所有的CPU都是对等的,通过总线连接共享一块物理内存,因而所有的CPU都共享全部资源。操作系统只管理一个队列,每个处理器依次处理队列中的进程。常见的PC都是这种架构,拓展性较差,因为随着CPU数量的增加,内存访问冲突将迅速增加,最终导致CPU资源的浪费,使CPU性能的有效性大大降低。(SMP副武器的CPU利用率,最好的情况是2~4个CPU)
NUMA:Non-Uniform Memory Access。NUMA是SMP的一种改进,它的基本特征是具有多个CPU模块,每个CPU模块由多个CPU(如4个)组成,并且具有独立的本地内存,IO槽口等。由于其节点之间可以通过互联模块进行连接和信息的交互,因此每个CPU可以访问整个系统的内存(与MPP的主要区别)。同时,访问本地内存的速度将远远高于访问远地内存(系统内其他节点的内存)的速度,这也是非一致存储访问NUMA的由来。由于这个特点,为了更好地发挥系统性能,开发应用程序时需要尽量减少不同CPU模块之间的信息交互。利用NUMA,可以较好地解决SMP的拓展问题,在一个服务器可以支持上百个CPU。但是在CPU数量增加到一定程度的时候,由于访问远地内存的速度较慢,这时候增加CPU带来的提升会降低。
MPP:Massive Parallel Processing。MPP提供了另外一种进行系统拓展的方式,它由多个SMP服务器通过一定的节点互联网进行连接,协同工作,完成相同的任务。其基本特征是由多个SMP服务器(一个SMP服务器称为一个节点)通过节点互联网络连接而成,每个节点只访问自己的本地资源,是一个完全无共享的结构,因而其拓展能力最好,理论上可以无限制拓展。也就是通过节点网络传输信息,这个过程称为数据重分配Data Redistribution。
三者都有不同的优势和缺点。SMP适合存储中心集中型的数据,比如中心数据库。NUMA适合OLTP事务处理环境,MPP适合决策和数据挖掘。
回到CLH锁,它的作用是改良自旋锁。之前我们提到过,自旋锁在多处理器下读写同一个变量,每次操作都在多个处理器缓存之间进行缓存同步,这会导致繁重的系统总线和内存的流量,大大降低系统整体的性能。解决这个问题有两种方法,一种是MCS锁,另一种是CLH锁。
①MCS锁。MCS Spinlock 是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,直接前驱负责通知其结束自旋,从而极大地减少了不必要的处理器缓存同步的次数,降低了总线和内存的开销。
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
public class MCSLock {
public static class MCSNode {
volatile MCSNode next;
volatile boolean isBlock = true; // 默认是在等待锁
}
volatile MCSNode queue;// 指向最后一个申请锁的MCSNode
private static final AtomicReferenceFieldUpdater UPDATER = AtomicReferenceFieldUpdater
.newUpdater(MCSLock.class, MCSNode.class, "queue");
public void lock(MCSNode currentThread) {
MCSNode predecessor = UPDATER.getAndSet(this, currentThread);// step 1
if (predecessor != null) {
predecessor.next = currentThread;// step 2
while (currentThread.isBlock) {// step 3
}
}else { // 只有一个线程在使用锁,没有前驱来通知它,所以得自己标记自己为非阻塞
currentThread. isBlock = false;
}
}
public void unlock(MCSNode currentThread) {
if (currentThread.isBlock) {// 锁拥有者进行释放锁才有意义
return;
}
if (currentThread.next == null) {// 检查是否有人排在自己后面
if (UPDATER.compareAndSet(this, currentThread, null)) {// step 4
// compareAndSet返回true表示确实没有人排在自己后面
return;
} else {
// 突然有人排在自己后面了,可能还不知道是谁,下面是等待后续者
// 这里之所以要忙等是因为:step 1执行完后,step 2可能还没执行完
while (currentThread.next == null) { // step 5
}
}
}
currentThread.next.isBlock = false;
currentThread.next = null;// for GC
}
}
②CLH锁。CLH锁也是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,它不断轮询前驱的状态,如果发现前驱释放了锁就结束自旋。
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
public class CLHLock {
public static class CLHNode {
private volatile boolean isLocked = true; // 默认是在等待锁
}
@SuppressWarnings("unused" )
private volatile CLHNode tail ;
private static final AtomicReferenceFieldUpdater<CLHLock, CLHNode> UPDATER = AtomicReferenceFieldUpdater
. newUpdater(CLHLock.class, CLHNode .class , "tail" );
public void lock(CLHNode currentThread) {
CLHNode preNode = UPDATER.getAndSet( this, currentThread);
if(preNode != null) {//已有线程占用了锁,进入自旋
while(preNode.isLocked ) {
}
}
}
public void unlock(CLHNode currentThread) {
// 如果队列里只有当前线程,则释放对当前线程的引用(for GC)。
if (!UPDATER .compareAndSet(this, currentThread, null)) {
// 还有后续线程
currentThread. isLocked = false ;// 改变状态,让后续线程结束自旋
}
}
}
二者图示比较:
差距:
①CLH是在前驱节点的属性上自旋,而MCS是在本地属性变量上自旋
②CLH锁释放时,只需要改变自己的属性,而MCS锁释放则需要改变后继节点的属性。
③从链表队列来看,CLH的队列是隐式的,CLH的Node并不实际持有下一个节点,而MCS的队列是物理存在的。
显然,根据我们前面所说的CPU架构,MCS适合NUMA架构,避免读取其他内存块时的巨大开销。而CLH锁无须考虑这个层次,一般都是SMP架构。于是AQS的实现使用CLH锁,改进了自旋锁,并且无须考虑NUMA架构的情况下改用MCS锁的情况,代码更简单。CLH需要记录前继节点和后继节点,因而,CLH锁使用双向队列。
AQS通过头尾指针来管理同步队列,同时实现包括获取锁失败的线程进行入队,释放锁时对同步队列中的线程进行通知等核心方法。示意图如下:
节点的入队和出队,就是对应着锁的获取和释放两个操作。获取锁失败进行入队操作,获取锁成功进行出队操作。
常用方法
AQS的模板方法:同步队列,锁的获取和释放,锁的等待,对线程同步状态的管理,对阻塞队列进行派对,等待通知。
常用方法:acquire,release,acquireInterruptibly,acquireShared,releaseShared等
①acquire
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
当锁调用lock方法时,表明它要获取一个独占锁。如果获取失败,就将当前线程加入到同步队列,如果成功直接执行。而lock方法实际上会调用AQS的acquire方法。首先是通过!tryAcquire(arg)
看同步状态是否获取成功,如果获取成功就直接结果并返回了。如果失败,就执行acquireQueued(addWaiter(Node.EXCLUSIVE), 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;
}
if (shouldParkAfterFailedAcquire(p, node) && // 获取锁失败,判断是否要阻塞
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
大概就是一个无限循环,一直在尝试获取锁tryAcquire,如果获取成功就直接返回该线程,然后让该线程去执行。如果获取锁失败,此时会调用shouldParkAfterFailedAcquire方法,主要是靠前驱节点来判断节点当前线程是否应该被阻塞。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus; // 前驱节点的状态
if (ws == Node.SIGNAL) // 前驱节点为SIGNAL,表示当前线程处于等待状态,直接返回true
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) { // 大于0表示状态为CANCELLED,表示该节点已经超时或者被中断了,需要从同步队列中取消
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else { // 前驱节点为Condition / Propagate,此时线程需要等待SIGNAL信号,但还没有停止
/*
* 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.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
这段代码主要检查当前线程是否需要被阻塞,具体规则如下:
1.如果当前线程的前驱节点状态为SIGNAL,则表明当前线程需要被阻塞,调用unpark方法唤醒,直接返回true,当前线程被阻塞。
2.如果当前线程的前驱节点状态为CANCELLED,则表明该线程的前驱节点已经等待超时或者被中断了,此时需要从CLH队列中将该前驱节点删除,直接回溯到前驱节点状态 <= 0,返回false
3.如果前驱节点的状态既不是SIGNAL,也不是CANCELLED,则通过CAS的方式将其前驱节点设置其SIGNAL,返回false
如果shouldParkAfterFailedAcquire返回true,此时将调用parkAndCheckInterrupt方法将当前线程挂起,从而阻塞线程的调用栈,并同时返回当前线程的中断状态。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
最后调用的是LockSupport工具类进行创建锁和实现线程阻塞,LockSupport里调用的也是Unsafe类。
这时候我们理解了acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
中的acquireQueued方法,那么addWaiter又是什么方法?从acquireQueued的参数列表就可以看出来,addWaiter方法返回一个Node,而它的方法名表示它会添加一个节点到同步等待队列中,Node.EXCLUSIVE表明加入的是独占锁。看源码:
private Node addWaiter(Node mode) {
// 将当前线程构建成Node类型
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) { // 判断尾结点是否为null
node.prev = pred;
if (compareAndSetTail(pred, node)) { // 尾结点不为null,采用尾插法
pred.next = node; // 原先的tail的next指向node
return node;
}
}
enq(node); // 尾结点为null,说明同步队列为空,直接加入即可
return node;
}
可以看到,addWaiter的逻辑:先判断尾结点是否为null,如果为null,直接调用enq方法插入。如果不为null,则通过CAS操作compareAndSetTail方法,使得当前同步队列的tail为node,并且原先的tail的next指向node。可是这里的CAS操作并没有放在循环里,也就是无法自旋。那么在CAS操作出错,返回false之后,仍然会执行到enq方法。因此addWaiter只尝试了一次快速的入队操作,如果失败了再进入到enq方法,这也是JDK注释所说的“Try the fast path of enq; backup to full enq on failure”。可以预知enq里仍然有初始化的时候,也有CAS自旋代码。enq方法源码:
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node())) // 队列为空,创建head节点,tail和head指向同一个
tail = head;
} else {
node.prev = t; // 尾插法
if (compareAndSetTail(t, node)) { // CAS操作,如果失败就自旋
t.next = node;
return t;
}
}
}
}
enq方法只有在第一个线程加入同步队列时,调用compareAndSetHead方法,完成链式队列的头结点初始化,之后通过自旋,不断尝试CAS尾插法,直到线程插入到同步队列为止。
acquire流程图:
②release
acquire是独占锁的获取过程,因为获取失败的时候还要加入到同步队列中,因此逻辑还是比较复杂的。而独占锁的释放就相对而言简单一点,需要调用的是release方法:
public final boolean release(long arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
先进行同步状态释放(tryRelease),如果释放成功,则会执行if块中的代码。当head指向的头结点不为null,且该节点的状态值不为0的话(如果为0,表示节点还在运行中,不release,也不释放锁),执行unparkSuccessor方法,如下:
/**
* Wakes up node's successor, if one exists.
*
* @param node the 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.
*/
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 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);
}
首先判断当前节点的状态,如果小于0,因为等一下我们就要释放它的后继节点了,所以这时候就把它的状态改为0compareAndSetWaitStatus(node, ws, 0);
。唤醒后继线程的逻辑有点意思,先判断是否为null,或者是否已经cancelled,如果是,那么将进入循环体,s = null;
意义不明。然后循环是从tail开始的,从队列结尾,一直往前面找,直到找到最前面的一个节点。然后对该节点调用LockSupport.unpark(s.thread);
,解除阻塞。
Question: unparkSuccessor为什么是从后往前寻找第一个非Cancelled的节点?
Ans:主要是enq方法里的compareAndSetTail方法,以及t.next = node;
,这两个步骤并不是原子性。如果在执行CAS,在重定向next指针之前,这时候执行了unparkSuccessor方法,此时next还没有指向正确的节点,导致无法从前往后找。此外,当一个节点的状态转为cancelled时,先断开的是next指针,prev指针并未断开。因而,入队的非原子性操作,以及cancelled节点的产生过程中断开的next指针的操作,会让此时从前往后遍历,无法遍历所有的节点。而从后往前遍历则避免了这个情况。·
③cancelAcquire
在acquire的最后,finally语句可能会调用cancelAcquire方法,用于生成CANCELLED状态节点。该方法逻辑:
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null) // 将无效节点过滤
return;
node.thread = null; // 设置该节点不关联任何线程,也就是虚节点
// Skip cancelled predecessors
Node pred = node.prev;
while (pred.waitStatus > 0) // 跳过前驱节点,跳过取消状态的node
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的状态为CANCELLED
// If we are the tail, remove ourselves.
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else { // 从后往前找第一个非CANCELLED状态的节点为尾结点
// 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 ||
(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 {
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
流程:
①获取当前节点的前驱节点,如果前驱节点的状态是CANCELLED,那就一直往前遍历,找到第一个waitStatus <= 0的节点,将找到的pred节点和当前Node相关联,并且把node的状态设置为CANCELLED
②根据当前节点的位置,分成三种情况。当前节点是头结点head的后继节点,尾结点,中间节点
(1)当前节点是尾结点(断开pred的next,然后修改tail为pred)
(2)当前节点是head的后继节点(当前节点的next指向自己,断开了next指针,prev没有改变)
(3)既不是head的后继节点,也不是尾结点(pred.next指向pred.next.next,然后当前节点的next转为指向自己)
可以看到,所有的变化都是对next指针进行操作,而没有对prev指针操作。
Question:为什么所有的cancel变化都是对对next操作,而没有对prev指针进行操作?什么情况下操作prev?
Ans:(1)执行cancelAcquire的时候,当前节点的前置节点可能已经从队列中出去了(已经执行过Try代码块中的shouldParkAfterFailedAcquire方法了),如果此时修改Prev指针,有可能会导致Prev指向另一个已经移除队列的Node,因此这块变化Prev指针不安全。
(2)shouldParkAfterFailedAcquire方法中,会执行
do {node.prev = pred = pred.prev;} while (pred.waitStatus > 0);
其实就是在处理Prev指针。shouldParkAfterFailedAcquire是获取锁失败的情况下才会执行,进入该方法后,说明共享资源已被获取,当前节点之前的节点都不会出现变化,因此这个时候变更Prev指针比较安全。