AbstractQueuedSynchronizer源码分析
前言
自旋锁
多线程场景下,当一个线程尝试获取锁时,如果锁被占用,就在当前线程循环检查锁是否被释放,此时当前线程并没有休眠或挂起。
代码实现
public class SpinLock {
/**
* AtomicReference保证了操作的原子性
*/
private AtomicReference<Thread> owner = new AtomicReference<>();
/**
* 获取锁
*/
public void lock() {
Thread currentThread = Thread.currentThread();
// 如果锁未被占用,则设置当前线程为锁的拥有者,设置成功返回true,否则返回false
// null为期望值,currentThread为要设置的值,如果当前内存值和期望值null相等,替换为currentThread
while (!owner.compareAndSet(null, currentThread));
}
/**
* 释放锁
*/
public void unlock() {
Thread currentThread = Thread.currentThread();
// 只有锁的拥有者才能释放锁,如果currentThread和owner持有的线程相等,则将owner持有的线程设置为null
owner.compareAndSet(currentThread, null);
}
}
缺点
- 非公平锁,不能保证等待线程按照FIFO获取锁。
- 由于执行线程均在同一个共享变量上自旋,申请和释放锁的时候需要对该共享变量进行更改,这将导致所有参与排队自旋操作的处理器的缓存失效。在竞争比较激烈的情况下,频繁的缓存同步操作会大大降系统的性能。
###CLH Lock
CLH lock is Craig, Landin, and Hagersten (CLH) locks, CLH lock is a spin lock, can ensure no hunger, provide fairness first come first service.
The CLH lock is a scalable, high performance, fairness and spin lock based on the list, the application thread spin only on a local variable, it constantly polling the precursor state, if it is found that the pre release lock end spin.
CLK lock是一个自旋锁,能够确保无饥饿性,提供先来先服务的公平性。CLK lock是一个基于链表的可扩展、高性能、公平的自旋锁,申请线程只在一个局部变量上自旋,它不断地轮询前驱节点的状态,如果发现前驱释放了锁,就结束自旋。
实现原理
- 当前线程持有自己的node变量,node中有一个locked属性,true代表需要锁,false代表获取锁。
- 当前线程持有前驱的node引用,轮询前驱node的locked属性,如果为true表示当前线程需要轮询等待前驱释放锁,flase表示前驱已经释放锁,当前线程结束自旋
- 维护一个FIFO的Node队列(链表头插法),tail始终指向最后加入的线程。
代码实现
public class CLHLock implements Lock {
/**
* 前驱节点
*/
private final ThreadLocal<Node> prev = new ThreadLocal<>();
/**
* 当前节点
*/
private final ThreadLocal<Node> node;
/**
* 指向最后加入的线程
*/
private final AtomicReference<Node> tail;
private static class Node {
/**
* 如果为true,以该节点为前驱的线程会一直自旋;
* 如果为false,以该节点为前驱的线程 结束/不会 自旋,表示获取锁成功;
*/
private volatile boolean locked;
}
public CLHLock() {
//初始化node
node = ThreadLocal.withInitial(Node::new);
//初始化tail,指向一个node,可以理解为head节点中的一个node,并且该节点locked属性为false
tail = new AtomicReference<>(new Node());
}
/**
* 获取锁
*/
public void lock() {
//获取表示当前线程状态的节点
final Node curNode = this.node.get();
//修改为true,表示当前线程需要获取锁
curNode.locked = true;
//获取这之前最后加入的线程,并把当前加入的线程设置为tail,
Node pred = this.tail.getAndSet(curNode);
//设置当前节点的前驱节点
this.prev.set(pred);
// 轮询前驱节点的locked属性,尝试获取锁
while (pred.locked);
}
/**
* 释放锁
*/
public void unlock() {
final Node curNode = this.node.get();
//解锁很简单,将节点locked属性设置为false
curNode.locked = false;
//当前节点设置为前驱节点,也就是上面初始化提到的head节点中的node
//如果没有此行,可能会造成死锁
this.node.set(this.prev.get());
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public boolean tryLock() {
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
@Override
public Condition newCondition() {
return null;
}
}
原理分析
假设现在有两个线程thread1,thread2尝试获取锁。示例图地址
- 1.初始化的时候
tail
指向head
节点,head
节点中node
的locked
属性为false
,preNode
指向null
。
- 2.当
thread1
进来的时候,thread1
持有的node
节点的locked
属性为true
,preNode
指向之前的head
节点中的node
节点。
- 3.当
thread2
进来的时候,thread2
持有的node
节点的locked
属性为true
,preNode
指向thread1
的node
节点,thread1
的node
节点locked
属性为true
,thread2
自旋。
- 4.
thread1
执行完后释放锁(修改locked
属性为false
),thread2
轮询到thread1
的node
节点locked
属性为false
,结束自旋。此处有一个细节需要注意一下:thread1
unlock最后要将node
更新为head中的node
节点,此时thread2
中prev
也指向head
中的node
节点,如果没有这步,如果thread1
执行比较快,unlock
之后又立刻lock
,此时thread1
变为tail
节点,thread1
的前驱为thread2
的node
,node
的locked值为true
,thread2
的前驱为的thread1
的node
,node
也为true
,此时就造成了死锁。
另一种实现方式
public class CLHLock2 implements Lock<CLHLock2.CLHNode>{
public static class CLHNode {
// 默认是在等待锁
private boolean isLocked = true;
}
//tail指向最后加入的线程node
private volatile CLHNode tail;
//AtomicReferenceFieldUpdater基于反射的实用工具,可以对指定类的指定 volatile 字段进行原子更新。
//对CLHLock2类的tail字段进行原子更新。
private static final AtomicReferenceFieldUpdater<CLHLock2, CLHNode> UPDATER = AtomicReferenceFieldUpdater
.newUpdater(CLHLock2.class, CLHNode.class, "tail");
/**
* 将node通过参数传入,其实和threadLocal类似,每个线程依然持有了自己的node变量
*
* @param currentThread
*/
public void lock(CLHNode currentThread) {
//将tail更新成当前线程node,并且返回前一个节点(也就是前驱节点)
CLHNode preNode = UPDATER.getAndSet(this, currentThread);
//如果preNode为空,表示当前没有线程获取锁,直接执行。
if (preNode != null) {
//轮询前驱状态
while (preNode.isLocked) ;
}
}
public void unlock(CLHNode currentThread) {
//compareAndSet,如果当前tail里面和currentThread相等,设置成功返回true,
// 表示之后没有线程等待锁,因为tail就是指向当前线程的node。
// 如果返回false,表示还有其他线程等待锁,则更新isLocked属性为false
if (!UPDATER.compareAndSet(this, currentThread, null)) {
currentThread.isLocked = false;// 改变状态,让后续线程结束自旋
}
}
}
测试
public class LockTest {
static int count = 0;
public static void testLock(Lock lock) {
try {
lock.lock();
System.out.println(Thread.currentThread().getName()+"获得锁");
for (int i = 0; i < 1000; i++) {
++count;
}
System.out.println("count="+count);
} finally {
System.out.println(Thread.currentThread().getName()+"释放锁");
lock.unlock();
}
}
public static void main(String[] args) {
final CLHLock clh = new CLHLock();
final CyclicBarrier cb = new CyclicBarrier(10, () -> {
System.out.println(count);
});
for (int i = 0; i < 10; i++) {
new Thread(() -> {
testLock(clh);
try {
cb.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}
).start();
}
}
}
结果:
优点
- 公平锁,FIFO
缺点
- NUMA系统结构下性能很差,在这种系统结构下,每个线程有自己的内存,如果前趋结点的内存位置比较远,自旋判断前趋结点的locked域,性能将大打折扣。
###MCS Lock
MCS Spinlock 是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,直接前驱负责通知其结束自旋(与CLH自旋锁不同的地方,不在轮询前驱的状态,而是由前驱主动通知),极大地减少了不必要的处理器缓存同步的次数,降低了总线和内存的开销
实现原理
- 1.每个线程持有一个自己的
node
,node
有一个locked
属性,true表示等待获取锁,false表示可以获取到锁,还有一个next
属性,用来指向其后继节点。 - 2.线程在轮询自己
node
的locked
状态,true
表示锁被其他线程占用,等待获取锁,自旋。 - 3.线程释放锁的时候,修改后继节点的
locked
属性,从而使后继节点结束自旋。 - 4.维护一个FIFO队列(尾插法),tail始终指向最后加入的线程节点。
代码实现
public class MCSLock implements Lock<MCSLock.MCSNode>{
public static class MCSNode {
//持有后继者的引用
MCSNode next;
// 默认是在等待锁
boolean locked = true;
}
/**
* 指向最后一个申请锁的MCSNode
*/
private volatile MCSNode tail;
//AtomicReferenceFieldUpdater基于反射的实用工具,可以对指定类的指定 volatile 字段进行原子更新。
//对MCSLock类的tail字段进行原子更新。
private static final AtomicReferenceFieldUpdater<MCSLock, MCSNode> UPDATER = AtomicReferenceFieldUpdater
.newUpdater(MCSLock.class, MCSNode.class, "tail");
public void lock(MCSNode currentThreadMcsNode) {
//更新tail为最新加入的线程节点,并取出之前的节点(也就是前驱)
MCSNode predecessor = UPDATER.getAndSet(this, currentThreadMcsNode);//step4
//前驱为空表示没有线程占用锁
if (predecessor != null) {
//将当前节点设置为前驱节点的后继者
predecessor.next = currentThreadMcsNode;//step5
//轮询自己的isLocked属性
while (currentThreadMcsNode.locked);
}else {
// 只有一个线程在使用锁,没有前驱来通知它,所以得自己标记自己为非阻塞 - 表示已经加锁成功
currentThreadMcsNode.locked = false;
}
}
public void unlock(MCSNode currentThreadMcsNode) {
// 锁拥有者进行释放锁才有意义
if(currentThreadMcsNode.locked){
return;
}
//UPDATER.get(this) 获取最后加入的线程的node(也就是tail指向的node)
//相同代表当前没其他有线程等待锁,进入下面的处理
if (UPDATER.get(this) == currentThreadMcsNode) {//step1
//这个时候可能会有其他线程又加入了进来,故需要检查next是否存在节点
if (currentThreadMcsNode.next == null) { //step2
//将tail设置为空,如果返回true设置成功,如果返回false,表示设置失败(其他线程加入了进来,使得当前tail持有的节点不等于currentThreadMcsNode)
if (UPDATER.compareAndSet(this, currentThreadMcsNode, null)) {// //step3
// 设置成功返回,没有其他线程等待锁
System.out.println(Thread.currentThread().getName() + "释放锁,且无后继节点");
return;
} else {
// 突然有其他线程加入,需要检测后继者是否有值,因为:step4执行完后,step5可能还没执行完
while (currentThreadMcsNode.next == null) {
}
}
}
}
//如果获取到的最后加入的node和当前node(currentThreadMcsNode)不相同,
// 表示还有其他线程等待锁,直接修改后继者的locked属性为false,通知后继者结束自旋
currentThreadMcsNode.next.locked = false;
//队列中移除当前节点 for GC
currentThreadMcsNode.next = null;
}
}
原理分析
假设thread1,thread2,thread3依次获取锁。
- 1.队列初始化时没有节点,
tail
指向null
。
- 2.
thread1
想要获取锁,将自己置于队尾,由于他是第一个节点,locked
为false
,next
指向null
。
- 3.
thread2
和thread3
相继加入队列,thread1->next=thread2
,thread2->next=thread3
,且thread2
和thread3
都没有获取到锁,所以locked
均为false
,tail
指向thread3
。
- 4.
thread1
释放锁之后,改变其next
节点thread2
的locked
为false,此时thread2
获取到锁。同时,thread1
的next置为null
,等待gc。
测试
public static void testLock(MCSLock lock) {
MCSLock.MCSNode mcsNode = new MCSLock.MCSNode();
try {
lock.lock(mcsNode);
System.out.println(Thread.currentThread().getName() + "获得锁");
for (int i = 0; i < 1000; i++) {
++count;
}
System.out.println("count=" + count);
} finally {
lock.unlock(mcsNode);
System.out.println(Thread.currentThread().getName() + "释放锁");
}
}
public static void main(String[] args) {
final MCSLock mcsLock = new MCSLock();
final CyclicBarrier cb = new CyclicBarrier(10, () -> {
System.out.println(count);
});
for (int i = 0; i < 5; i++) {
new Thread(() -> {
testLock(mcsLock);
try {
cb.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
int count = 0;
}
}
结果:
优点
- MSC是在自己的结点的locked域上自旋等待。正因为如此,它解决了CLH在NUMA系统架构中获取locked域状态内存过远的问题。
##简介
AbstractQueuedSynchronizer
,简称AQS
,抽象队列同步器,基于FIFO队列(CLH),封装了各种底层的同步细节,如JDK
中的ReentrantLock
、ReentrantReadWriteLock
、Semaphore
、CountDownLatch
等都是利用它实现的,子类通过继承同步器并需要实现它的方法来定义自己的同步工具。
AQS有两种模式:独占模式和共享模式。
- 独占模式:一个线程在进行某些操作的时候其他的线程都不能执行该操作,比如在同一时刻只能有一个线程持有锁。如
JUC
下的ReentrantLock
,ReentrantReadWriteLock.WriteLock
都是独占模式。- 共享模式:可以同时允许多个线程同时进行某种操作,比如一个锁能够被多个线程同时拥有。如
JUC
下的ReentrantReadWriteLock.ReadLock
,Semaphore
,CountDownLatch
,CyclicBarrier
都是共享模式。
根据锁的获取机制,又分为公平锁和非公平锁。
- 公平锁:通过CLH等待队列,等待线程按照先来先得的规则,公平的获取锁。
- 非公平锁:无视CLH等待队列直接获取锁。
AQS三个核心点:
- state: 同步状态,用来表示多线程下竞争的资源、等待的条件等。
- 队列: 通常以链表的形式实现,采用悲观锁的思想,认为当前线程所等待的资源、状态或者条件短时间内无法满足,需要将当前线程包装成某种类型的数据结构,放到一个队列中,当一定条件满足后,再从队列中取出。
- CAS: CAS可以保证在多个线程同时竞争某个资源时,同一个时刻,只有一个线程能成功,从而保证了线程安全,是由
Unsafe
工具类的compareAndSwap/compareAndSet
来实现的。其采用乐观锁的思想,因此常常伴随着自旋,如果发现当前无法成功地执行CAS
,则不断重试,直到成功为止,自旋的表现形式通常是一个死循环for(;;)
。
源码分析
AbstractOwnableSynchronizer
AQS继承了AbstractOwnableSynchronizer,可以设置和获取独占锁的拥有者线程。
/**
* 独占模式下,获取到同步状态的线程
*/
private transient Thread exclusiveOwnerThread;
methed | description |
---|---|
protected final void setExclusiveOwnerThread(Thread thread) | 设置独占模式下获取到同步状态的线程 |
protected final Thread getExclusiveOwnerThread() | 获取独占模式下获取到同步状态的线程 |
同步状态state
在
AQS
中维护了一个由volatile
修饰的state
字段,用它来表示同步状态。
/**
* The synchronization state.
*/
private volatile int state;
AQS
提供了几个访问这个字段的方法:
| methed | description|
| ----- | ----- |
|protected final int getState()
| 获取state
的值|
|protected final void setState(int newState)
|设置state
的值|
|protected final boolean compareAndSetState(int expect, int update)
|使用CAS
方式更新state
的值|
这几个方法都用final修饰,说明子类无法重写他们,protected修饰表示非子类不能够调用该方法。
我们可以通过修改
state
来实现多线程的独占模式和共享模式。
例如在独占模式下,我们可以把state
初始值设置为0
,每当某个线程要进行独占操作之前,先判断state
值是否为0
,如果不是0
意味着别的线程已经进入该操作,则该线程需要阻塞等待;如果是0
就把state
设置为1
,表示该线程进入独占操作。 这个先判断在设置的过程我们可以通过CAS
操作保证操作原子性,我们把这个过程称为尝试获取同步状态。如果一个线程获取同步状态成功了,那么另一个线程尝试获取同步状态时发现state
的值是1
,就会一直阻塞等待。直到获取同步状态成功的线程执行完毕同步操作时候释放同步状态,即把state
设置为0
,并通知后续等待的线程。
在共享模式下,比如时候某项操作我们允许
10
个线程同时进行,超过这个数量的线程就要阻塞等待。那么我们可以把state
初始值设置为10
,一个线程进行共享模式操作之前先判断state
的值是否大于0
,如果不大于0
表示当前已有10个线程在进行该操作,该线程需要阻塞等待,如果state
值大于0,可以把state
减1
后进入该操作,这个过程也已通过CAS
来保证操作的原子性,同理,这个过程也称为尝试获取同步状态。每当一个线程完成操作的时候,需要释放同步状态,把state
加1
,并通知后续等待的线程。
不同的同步工具针对的并发场景不同,所以如何尝试获取同步状态和如何释放同步状态是由
AQS
的子类自己实现的,在AQS
中只是定义了对应的方法:
| methed | description|
| ----- | ----- |
|protected boolean tryAcquire(int arg)
| 独占模式下尝试获取同步状态,成功返回true,失败返回false|
|protected boolean tryRelease(int arg)
|独占模式下释放同步状态,成功返回true,失败返回false|
|protected int tryAcquireShared(int arg)
|共享模式下尝试获取同步状态,大于等于0表示获取成功,小于0表示获取失败|
|protected boolean tryReleaseShared(int arg)
|共享模式下尝试释放同步状态,成功返回true,失败返回false|
|protected boolean isHeldExclusively()
|独占模式下,如果当前线程已经获取到同步状态,返回true,否则返回false|
以tryAcquire
为例:
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
AQS
虽然被定义为抽象类,但事实上它并不包含任何的抽象方法。这是因为AQS
是用来支持多种同步工具的,如果定义抽象方法,则子类在继承时必须要重写所有的抽象方法,这显然是不合理的。所以AQS
将需要子类重写的方法都设计成protect
方法,将其默认实现为抛出UnsupportedOperationException
异常。如果子类使用到这些方法,但是没有重写,就会抛出异常,如果子类没有使用到这些方法,则无需重写这些方法。
比如:如果我们自定义的同步工具需要在独占模式下工作,就重写
tryAcquire
、tryRelease
、isHeldExclusively
方法,如果在共享模式下工作,就重写tryAcquireShared
、tryReleaseShared
方法,实际上是对设计模式中模板方式模式
的应用。
模板方法模式:在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
同步队列
AQS
中的使用的同步队列是CLH
的一个变种,主要用来存储这些等待的线程 ,但它并不是直接存储线程,而是存储拥有线程的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条件上等待阻塞(用于Condition的await等待)
static final int CONDITION = -2;
// waitStatus变量的值,标志着下一个acquireShared方法线程应该被允许。(用于共享模式)
static final int PROPAGATE = -3;
// 标记着当前节点的状态,默认状态是0, 小于0的状态都是有特殊作用,大于0的状态表示已取消
volatile int waitStatus;
// prev和next实现一个双向链表
volatile Node prev;
volatile Node next;
// 该节点拥有的线程
volatile Thread thread;
//如果是SHARED,表示当前节点是共享模式,如果是null,当前节点是独占模式,如果是其他值,当前节点也是独占模式,不过这个值也是Condition队列的下一个节点。
Node nextWaiter;
}
waitStatus有五种状态
- CANCELLED:值为1。场景:当该线程
等待超时
或者被中断
,需要从同步队列中取消等待,则该线程被置1,即被取消(这里该线程在取消之前是等待状态),即放弃获取锁。
- SIGNAL:值为-1。场景:后继节点处于阻塞状态,当前节点的线程如果释放了同步状态或者被取消(当前节点状态置为-1),将会唤醒其后继节点。
- CONDITION:值为-2。场景:节点处于条件队列中,节点线程等待在
Condition
上,当其他线程对Condition
调用了signal
方法后,该节点从条件队列中转移到同步队列中;
- PROPAGATE:值为-3。场景:表示下一次的共享状态会被无条件的传播下去,即下一个
acquireShared
方法线程应该被允许。(用于共享模式)
- 初始值:值为0,初始状态。
nextWaiter:如果是
SHARED
,表示当前节点是共享模式,如果是null
,当前节点是独占模式,如果是其他值,当前节点也是独占模式,不过这个值也是Condition
队列的下一个节点。
注意:通过
Node
我们可以实现两个队列,一是通过prev
和next
实现同步队列(双向队列),二是nextWaiter
实现Condition
条件上的条件队列(单向队列)。
AQS中操作同步队列
AQS
中定义一个头结点引用和一个尾节点引用,通过这两个节点就可以进行Node
节点的入队和出队操作。即当一个线程尝试获取同步状态失败之后,就把这个线程阻塞并包装成Node
节点插入到这个同步队列中,当获取同步状态成功的线程释放同步状态的时候,同时通知在队列中的下一个未获取到同步状态的节点,让该节点的线程再次去获取同步状态。
存储同步队列
/**
* 不包装任何线程,是一个dummy node(哑节点)
*/
private transient volatile Node head;
/**
* 指向最新加入的线程node
*/
private transient volatile Node tail;
将当前线程添加到同步队列队尾
/**
* 将当前线程包装为node添加到同步队列队尾
* @param node 独占模式Node.EXCLUSIVE或共享模式Node.SHARED
* @return 包装当前线程的节点
*/
private Node addWaiter(Node mode) {
// 将当前线程包装为node节点
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;
}
}
// 两种情况:
// 1.队列为空
// 2.compareAndSetTail失败(可能存在多个线程同时更新)
// 调用enq,并插入新的节点。
enq(node);
return node;
}
关于快速尝试快速尝试添加尾节点,enq方法已经包含这块逻辑,为什么单独再写一次?
这种做法在并发编程中比较常见,即把最有可能成功执行的代码直接写在最常用的调用处,和C++
中的inline
方法一个意思。因为在线程数不多的情况下,CAS还是很难失败的,这样的话就不用再调用enq
方法。因此这种写法可以,节省多条指令。而调用enq需要一次方法调用,进入循环,比较null,然后才执行这块逻辑。所以这样做能够节省指令,提高效率。
将一个节点插入到同步队列尾
/**
* 通过自旋+CAS的方式,确保当前节点入队。
* @param node 包装当前线程的节点
* @return 原先队列尾节点,当前节点的前驱节点
*/
private Node enq(final Node node) {
for (;;) {
Node t = tail;
// t为null,表示队列为空,先初始化队列
//队列不是在构造的时候初始化的,而是延迟到需要使用的时候再初始化
if (t == null) {
// 采用CAS函数即原子操作方式,设置队列头head值。
// 如果成功,再将head值赋值给链表尾tail。如果失败,表示head值已经被其他线程初始化,那么就进入下一次循环
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 到这里说明队列已经不为空,这个时候再尝试将当前节点加入队尾,分为三步:
// 1.将当前节点前驱指向当前的尾节点
node.prev = t;
// 2.CAS更新尾节点为当前节点
if (compareAndSetTail(t, node)) {
// 3.原先尾节点后继指向当前节点
t.next = node;
return t;
}
}
}
}
将当前节点node添加同步队列队尾分为三步:
- 设置node的前驱节点为当前的尾节点,
node.prev = t
;- 修改tail属性,指向当前节点;
- 修改原来的尾节点,使它的next指向当前节点;
整体流程入下图所示:
示例图地址
**需要注意的是:**这三步并不是一个原子操作,第一步很容易成功,第二步由于是一个
CAS
操作,并发条件下可能会失败,第三步只有在第二步成功的条件下才执行。这里的CAS
保证了同一时刻只有一个节点能成为尾节点,其他节点将失败,失败后将回到for
循环中继续重试。
所以,当有大量的线程同时入队时,同一时刻,只有一个线程完整地完成这三步,而其他线程只能完成第一步,如下如所示:
注意:这里第三步是在第二步执行成功后才执行的,并且不是原子性的,这就意味着,即使我们已经完成了第二步,将新节点设置为尾节点,此时原来的尾节点next值可能还是
null
(因为第三步还没来得及执行),所以如果此时线程恰好从头节点开始向后遍历尾节点,那么它是不会遍历新加进来的尾节点的,但这显然是不合理的,因为现在的tail
已经指向了新节点。
但是,当我们完成第二步之后,第一步肯定是已经完成的,所以如果我们从尾节点开始向前遍历,就可以遍历到所有的节点。这也就是为什么我们在
AQS
中常常看到从尾节点开始逆向遍历链表,因为一个节点要能入队,它的prev
属性一定是有值的,但是它的next
属性可能暂时还没值。
至于那些
CAS
失败的节点,在下一轮循环中,它们的prev
属性会重新指向尾节点,继续尝试CAS
操作,最终所有的节点通过自旋不断的尝试入队,知道成功为止。
设置同步队列头节点
/**
* 通过CAS函数设置head值,仅仅在enq方法中调用
*/
private final boolean compareAndSetHead(Node update) {
return unsafe.compareAndSwapObject(this, headOffset, null, update);
}
设置同步队列尾节点
/**
* 通过CAS函数设tail值,仅仅在enq方法中调用
*/
private final boolean compareAndSetTail(Node expect, Node update) {
return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}
独占模式
在独占模式下,同一时刻只能有一个线程获取到同步状态,其他同时获取同步状态的线程被包装成一个
Node
节点放到同步队列中,直到获取到同步状态的线程释放同步状态才能继续执行。
核心方法
methed | description |
---|---|
public final void acquire(int arg) | 独占模式下获取同步状态,如果获取成功则返回,如果失败则将当前线程包装成Node 节点插入到同步队列中 |
public final void acquireInterruptibly(int arg) | 同上,只不过一个线程在执行本方法过程中被别的线程中断则抛出InterruptedException 异常 |
public final boolean tryAcquireNanos(int arg, long nanosTimeout) | 在上个方法的基础上加了超时限制,如果在给定时间内没有获取到同步状态返回false,否则返回true |
public final boolean release(int arg) | 独占模式下释放同步状态 |
获取同步状态
acquire
方法
public final void acquire(int arg) {
// 1.先调用tryAcquire方法,尝试获取同步状态,返回true则直接返回
// 2. 调用acquireQueued方法,先调用addWaiter方法为当前线程创建一个独占模式下的(Node.EXCLUSIVE)节点node,nextWaiter为null,并插入队列中,
// 然后调用acquireQueued方法去获取同步状态,如果不成功,就会让当前线程阻塞,当同步状态释放时才会被唤醒。
// acquireQueued方法返回值表示在线程等待过程中,是否有另一个线程调用该线程的interrupt方法,发起中断。
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
//不会立即响应中断的,而是在获取资源后进行自我中断处理
selfInterrupt();
}
tryAcquire
方法
/**
* 尝试去获取同步状态,立即返回,如果返回true表示获取成功。需要子类去重写
* @param arg
* @return
*/
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
acquireQueued
方法
/**
* 获取同步状态,如果没有获取到,就让当前线程阻塞等待
* @param node 包装当前线程的节点
* @param arg
* @return 该线程是否被中断
*/
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
// 判断自旋过程中是否被中断过
boolean interrupted = false;
for (;;) {
//node节点的前驱节点
final Node p = node.predecessor();
// 如果前驱节点是头节点head,并且尝试获取同步状态成功
// 那么当前线程就不需要阻塞等待,继续执行
if (p == head && tryAcquire(arg)) {
//重新设置队列头head
//注:这里是获取到同步状态之后的操作,不需要并发控制
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
该方法需要注意的几点:
- 能执行到该方法,说明
addWaiter
已经成功将包装了当前线程的节点添加到了同步队列队尾;- 该方法将再次尝试获取同步状态;
- 再次尝试获取同步状态失败后,判断是否需要把当前线程挂起;
为什么前面获取同步状态失败了,这里还要再次尝试获取同步状态?
首先,这里再次尝试获取同步状态是基于一定的条件的,即:当前节点的前驱节点是
head
节点。我们知道,head
节点就是个哑节点,它不代表任何线程,如果当前节点就是head
节点,那就说明当前节点已经排在整个同步队列的最前面了。这个主要是怕之前获取同步状态的线程很快就把同步状态给释放了,所以在当前线程阻塞之前抱着侥幸
的心理再试试能不能成功获取到同步状态,如果侥幸获取,就把头结点换成自己。
setHead
方法
/**
* 重新设置队列头head,它只在acquire系列的方法中调用
* @param node 包装当前线程的节点
*/
private void setHead(Node node) {
//头结点指向当前节点
head = node;
//因为该线程已经获取到同步状态,故线程也置为null
node.thread = null;
node.prev = null;
}
示例
假设现在线程
t0
、t1
同时获取同步状态,线程t0
获取到同步状态,线程t1
此时执行addWaiter
,因为此时尾节点tail
为null,所以调用enq
,进入自旋,第一次自旋先创建头节点dummy node
,第二次自旋包装线程t1为node1,并通过CAS
加入队尾,最后返回尾节点也就是线程t1对应的节点node1
。
接着,调用
acquireQueued
,此时,如果新加入的节点的前驱是头结点的话,会再次调用tryAcquire
尝试获取同步状态,因为可能之前成功获取同步状态的线程t0
已经把同步状态释放,所以在线程t1阻塞之前抱着侥幸的心理再试试能不能成功获取到同步状态。
如果获取成功,说明此时线程
t0
已经释放同步状态并且没有其他线程与之竞争,然后执行setHead
,将head
指向已经获得同步状态的节点node1
,node1
对应的thread
属性设置为null
,prev
属性设置为null
,这某种意义上导致node1
节点又成了一个哑节点,不代表任何线程,而之前的head
节点此时无任何引用,方便GC时能够将其回收。
为什么要这样做呢?
因为在
tryAcquire
调用成功后,exclusiveOwnerThread
属性已经记录了当前成功获取同步状态的线程了,此处无需再记录,也就是将当前线程从同步队列中出队。
为什么调用
setHead
而不是compareAndSetHead
?
在
enq
方法中,当我们设置头结点时,是新加一个哑节点并作为头结点,这个时候,可能多个线程都在执行这一步,所以需要通过CAS
保证只有一个线程能操作成功。
而在acquireQueued
方法里,由于我们在调用setHead
,已经通过tryAcquire
成功获取到了同步状态,意味着:
- 此时没有其他线程在创建头结点,因为队列此时并不是空的。
- 此时能执行
setHead
的只有一个线程,即当前线程。
所以,整个if
语句块内的代码是线程安全的,不需要采用CAS
操作。
shouldParkAfterFailedAcquire
方法
/**
* 根据前一个节点pred的状态,来判断当前线程是否应该被阻塞
* @param pred 当前线程前驱节点
* @param node 包装当前线程的节点
* @return 表示当前线程是否应该被阻塞
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//获取前驱节点的状态
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
// 如果前驱节点pred的状态是Node.SIGNAL,那么直接返回true,当前线程应该被阻塞
return true;
if (ws > 0) {
// 如果前驱节点状态是Node.CANCELLED(大于0就是CANCELLED),
// 表示前驱节点所在线程已经被唤醒了,要从同步队列中移除CANCELLED的节点。
// 所以从pred节点一直向前查找直到找到不是CANCELLED状态的节点,并把它赋值给node.prev,
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 此时前驱节点pred的状态只能是0或者PROPAGATE,不可能是CONDITION状态
// CONDITION(这个是特殊状态,只存在于condition队列中)
// 将前驱节点pred的状态设置成Node.SIGNAL,这样在下一次循环时,就是直接阻塞当前线程
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
parkAndCheckInterrupt
方法
/**
* 阻塞当前线程,线程被唤醒后返回当前线程中断状态
*/
private final boolean parkAndCheckInterrupt() {
// 通过LockSupport.park方法,阻塞当前线程,不再往下执行
LockSupport.park(this);
// 当前线程被唤醒后,返回当前线程中断状态,并清除当前线程的中断标志位
return Thread.interrupted();
}
有关
LockSupport
的介绍见LockSupport源码分析。
示例
接着上面的示例,如果获取失败,说明此时线程t0还没有释放同步状态,或者其他线程已经获取到了同步状态。此时调用
shouldParkAfterFailedAcquire
判断在获取同步状态获取失败后,是否需要将当前线程挂起。判断的依据就是前驱节点waitStatus
的值。
独占模式下,
waitStatus
有三个,分别是SIGNAL(-1)
、CANCELLED(1)
和0。0是节点初始化时的值,CANCELLED
表示Node
所代表的线程已经取消了排队,即放弃获取同步状态。SIGNAL
表示当前节点释放同步状态或放弃获取同步状态时,需要唤醒它的后继节点。
需要注意的是,
SIGNAL
这个状态不是节点自己给自己设置的,而是其后继节点设置的,举个例子:同学A昨晚通宵玩游戏,第二天一大早来上高数课,课上老师会按座位轮流叫学生回答问题,但是同学A表示太累了就对声旁的同步B说:我先趴会,老师叫你回答问题的时候(realse)叫我一下,然后同学A就把同学B的waitStatus
设置为SIGNAL
(如果此时同学B刚好有事,准备翘课(CANCELLED),临走之前也得把同学A叫醒)。
也就是说,我们决定讲一个线程挂起之前,首先要确保自己的前驱节点的
waitStatus
为SIGNAL
,这就相当于给自己设了一个闹钟再去睡,这个闹钟会在恰当的时候叫醒自己,否则,如果如果一直没有人来叫自己,自己可能会一直睡下去。所以也就有了后面的可xi中断的获取同步状态acquireInterruptibly
,带有超时的获取同步状态tryAcquireNanos
。
shouldParkAfterFailedAcquire
执行逻辑:
- 如果前驱节点
waitStatus
值为Node.SIGNAL
,则直接返回true
;- 如果前驱节点
waitStatus
值为Node.CANCELLED
(ws > 0),则跳过那些节点重新寻找正在等待中的节点。问题:ws为CANCELLED的节点啥时被出现的?后面分析cancelAcquire
会讲到。- 其他情况,将前驱节点的
waitStatus
通过CAS
操作改为Node.SIGNAL
,返回false
。
示例图如下图所示,包装线程t1
节点的前驱节点waitStatus
由0变为SIGNAL(-1)
,shouldParkAfterFailedAcquire
返回false
,然后进入acquireQueued
下次循环,因为线程t1
的前驱是head
节点,所以再次尝试获取同步状态,如果获取同步状态失败,则再次调用shouldParkAfterFailedAcquire
,因为此时包装线程t1
节点的前驱节点waitStatus
是SIGNAL(-1)
,所以该方法返回true
,然后调用parkAndCheckInterrupt
,将线程t1
挂起,后面的Thread.interrupted()
也不会被执行,直到其他线程unpark
了线程t1
或者线程t1
被中断。
如果此时再新来一个线程
t2
调用acquire
获取同步状态,它同样会被包装成node2
插入同步队列,此时其前驱节点node1
的waitStatus
也变为-1,此时node1
和node2
均处于等待状态。如下图所示:
acquireInterruptibly
方法
/**
* 可响应中断的获取同步状态
* @param arg
* @throws InterruptedException 当前线程被中断抛出中断异常
*/
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
doAcquireInterruptibly
方法
/**
* Acquires in exclusive interruptible mode.
* @param arg the acquire argument
*/
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);
}
}
doAcquireInterruptibly
整体逻辑和acquireQueued
相似,唯一不同的地方在于线程在同步队列中等待的过程中如果被中断(parkAndCheckInterrupt
返回true),会抛出InterruptedException
异常。
tryAcquireNanos
方法
/**
* 可超时的获取同步状态
* @param arg
* @param 获取同步状态的等待时间
* @throws InterruptedException 当前线程被中断抛出中断异常
*/
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
doAcquireNanos
方法
/**
* @param arg
* @param 超时时间
* @return
*/
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();
// 已经超时,返回false
if (nanosTimeout <= 0L)
return false;
// 如果没有超时,则等待nanosTimeout纳秒
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
//判断线程是否被中断
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
/**
* 线程自旋阈值
*/
static final long spinForTimeoutThreshold = 1000L;
注意:如果超时时间
nanosTimeout
小于spinForTimeoutThreshold(1000)
n,当前线程不需要park
,而是直接进入快速自旋的过程,原因在于spinForTimeoutThreshold
已经非常小了,非常短的时间等待无法做到十分精确,如果这时再次park
,会让nanosTimeout
的超时从整体上表现得不是那么精确,所以在超时非常短的场景中,选择无条件的快速自旋。
cancelAcquire
方法
在
acquireQueued
、doAcquireInterruptibly
、doAcquireNanos
三个方法中的finally
块中,都使用了该方法。cancelAcquire
只有在上面三个方法抛出异常的时候(failed = true)才会被调用,我们先分析一下可能抛出异常的地方:
- 子类实现
tryAcquire
时可能抛出异常。doAcquireInterruptibly
、doAcquireNanos
中由于当前线程被中断抛出中断异常。doAcquireNanos
中剩余超时时间(nanosTimeout
)小于等于零的时候。
均表示当前线程放弃获取同步状态(无论是主动放弃还是被动放弃)。
主要操作分为两部分:
- 清理状态:
- node不再关联到任何线程
- node的waitStatus置为CANCELLED
- node(预)出队
包括三个场景:
- node是tail
- node既不是tail,也不是head的后继节点
- node是head的后继节点
/**
* 将node节点的状态设置成CANCELLED,表示node节点所在线程已取消,不需要唤醒了。
* @param node 当前线程节点
*/
private void cancelAcquire(Node node) {
// 如果node为null,就直接返回
if (node == null)
return;
// node不再关联到任何线程
node.thread = null;
// 跳过已取消的节点,在队列中找到在node节点前面的第一个状态不是已取消的节点
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// 记录pred的前驱节点,用于CAS更新时使用
Node predNext = pred.next;
// 将node节点状态设置为已取消Node.CANCELLED;
node.waitStatus = Node.CANCELLED;
// 如果node节点是队列尾节点,那么就将pred节点设置为新的队列尾节点
if (node == tail && compareAndSetTail(node, pred)) {
// 并且设置pred节点(tail)的下一个节点next为null
compareAndSetNext(pred, predNext, null);
} else {
int ws;
// 如果node既不是tail,又不是head的后继节点
//则将node的前趋节点的waitStatus置为SIGNAL
//并使node的前趋节点指向node的后继节点
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 {
// 如果node是head的后继节点或者状态判断或设置失败,则直接唤醒node的后继节点
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
unparkSuccessor
方法
/**
* 唤醒node节点的下一个非取消状态的节点所在线程
* @param node 当前线程节点
*/
private void unparkSuccessor(Node node) {
// 获取node节点的状态
int ws = node.waitStatus;
// 如果小于0,就将状态重新设置为0,表示这个node节点已经完成了
// cancelAcquire中不会执行这一步,因为waitStatus为1
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 获取node节点的后继节点
Node s = node.next;
// 如果后继节点为null,或者状态是已取消,那么就要寻找下一个非取消状态的节点
if (s == null || s.waitStatus > 0) {
// 先将s设置为null,s不是非取消状态的节点(此处必须设置为null)
s = null;
// 从队列尾向前遍历,找到node的下一个有效节点
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
// 如果s不为null,表示存在非取消状态的节点。那么调用LockSupport.unpark方法,唤醒这个节点的线程
if (s != null)
LockSupport.unpark(s.thread);
}
示例
原始同步队列:
1.node
是tail
,t3
执行cancelAcquire
cancelAcquire
执行完毕后,node3
已经没有被其他地方引用,所以可以被回收。
2.node
既不是tail
,也不是head
的后继节点,t2
执行cancelAcquire
问题1:
cancelAcquire
执行完毕后,node2
实际上还未出队,因为node3
的prev
属性还指向node2
,那么node2
什么时候出队呢?
回答: 实际上在其他线程调用cancelAcquire
或者shouldParkAfterFailedAcquire
时,会根据prev
指针跳过CANCELLED
状态的节点,这也就回答了***shouldParkAfterFailedAcquire
那块提出的问题。***
**问题2:**为什么从队列尾向前遍历,找到node节点的下一个有效节点?
回答:CANCELLED
状态的节点的next
属性指向的本身,如果从node
开始从前往后遍历,如果遇到CANCELLED
节点,就会造成死循环。
3.node
是head
的后继节点,t3
执行行cancelAcquire
因为
head
节点的后继节点node1
对应的线程t1
放弃获取同步状态,所以需要唤醒node1的下一个有效节点所在线程,也就是t2
。然后t2
执行acquireQueued
中的for
循环,但此时node2
的前驱还是node1
,不是head
,所以t2
再次执行shouldParkAfterFailedAcquire
,调整node1
的prev
指针和head
的next
指针,此时node1
才算真正出队,然后下一次循环t2
采取真正的尝试获取同步状态。
总结
tryAcquire
方法在独占模式下尝试获同步状态,需由子类实现acquireQueued
方法,如果当前节点的前驱节点是头节点,并且能够获得同步状态的话,当前线程能够获得锁该方法执行结束退出;
获取同步状态失败的话,先将前驱节点状态设置成SIGNAL
,然后调用LookSupport.park
方法使得当前线程阻塞。- 如果线程获取同步状态期间发生异常,调用
cancelAcquire
,表示该线程放弃获取同步状态。- acquire流程图
acquireQueued
、doAcquireInterruptibly
、doAcquireNanos
原理相似,代码结构均如下所示:
boolean failed = true;
try {
for (;;) {
...
}
} finally {
if (failed)
cancelAcquire(node);
}
释放同步状态
release
方法
/**
* 独占模式下释放同步状态
* @param arg the release argument.
* @return true or false
*/
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
// 如果队列头节点的状态不是0,那么队列中就可能存在需要唤醒的等待节点。
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
之前,在
acquireQueued
获取同步状态的方法中,如果节点node
没有获取到同步状态, 那么我们会将节点node
的前驱状态设置为Node.SIGNAL
,然后调用parkAndCheckInterrupt
方法将节点node
所在线程阻塞。 在这里就是通过unparkSuccessor
方法,进而调用LockSupport.unpark(s.thread)
方法,唤醒被阻塞的线程。unparkSuccessor
之前已经分析过,此处不再多述。
tryRelease
方法
/**
* 独占模式下尝试释放同步状态,需要子类去重写
* @param arg the release argument.
* @return true or false
*/
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
示例
原始同步队列
假设现在线程t0
释放了同步状态,head
节点waitStatus
变为0
,同时唤醒t1
。
总结
- 调用
tryRelease
方法去释放当前持有的同步状态。- 如果完全释放了同步状态,那么就调用
unparkSuccessor
方法,去唤醒同步队列中head
的后继节点。
共享模式
共享模式获取与独占模式获取的最大不同就是在同一时刻可以有多个线程同时获取到同步状态,获取同步状态失败的线程也需要被包装成Node节点后阻塞。
核心方法
methed | description |
---|---|
public final void acquireShared(int arg) | 共享模式下获取同步状态,如果获取成功则返回,如果失败则将当前线程包装成Node 节点插入到同步队列中 |
public final void acquireSharedInterruptibly(int arg) | 同上,只不过一个线程在执行本方法过程中被别的线程中断则抛出InterruptedException 异常 |
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout) | 在上个方法的基础上加了超时限制,如果在给定时间内没有获取到同步状态返回false,否则返回true |
public final boolean releaseShared(int arg) | 共享模式下释放同步状态 |
####获取同步状态#####
acquireShared
方法
public final void acquireShared(int arg) {
// 尝试去获取同步状态,如果返回值小于0表示获取失败
if (tryAcquireShared(arg) < 0)
// 调用doAcquireShared方法去获取共享同步状态
doAcquireShared(arg);
}
共享模式下,调用
tryAcquireShared
方法尝试获取同步状态,如果返回值小于0表示获取失败,则继续调用doAcquireShared
方法获取同步状态。
tryAcquireShared
方法
/**
* 尝试去获取同步状态,如果返回值小于0表示获取失败,需要子类重写
* @param arg 当前线程要占用的资源数量
* @return
*/
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
tryAcquireShared
返回的是一个整型值:
- 如果该值小于0,表示当前线程获取同步状态失败。
- 如果该值大于0,表示当前线程获取同步状态成功,并且它后续的等待的也有可能获取同步状态成功,所以需要huan
- 如果该值等于0,表示当前线程获取同步状态成功,但它后续的等待线程无法继续获取同步状态,即不需要唤醒后续等待的线程。
因此,只要该返回值大于等于0,就表示获取同步状态成功。
doAcquireShared
方法
/**
* 先把当前节点加入到同步队列尾部,然后进入自旋,自选的作用:
* 获取同步状态或陷入阻塞
* @param arg 当前线程要占用的资源数量
* @return
*/
private void doAcquireShared(int arg) {
// 共享模式下,为当前线程创建节点node
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);
// 如果返回值大于等于0,表示获取同步状态成功
// 等于0表示不用唤醒后继节点,大于0需要
if (r >= 0) {
//设置当前节点为头结点,并且可能会继续唤醒后继的节点
setHeadAndPropagate(node, r);
p.next = null; // help GC
// 如果是因为中断醒来则设置中断标记位
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
//同步状态获取失败,调用parkAndCheckInterrupt方法阻塞当前线程,同时将前驱节点的状态改为Node.SIGNAL,
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
// failed为true,表示发生异常,
// 则将node节点的状态设置成CANCELLED,表示node节点所在线程已取消,不需要唤醒了
if (failed)
cancelAcquire(node);
}
}
setHeadAndPropagate
方法
/**
* 设置头结点唤醒后继节点
* @param node 包装了当前成功获取同步状态的线程
* @param propagate tryAcquireShared方法的返回值,可能大于0也可能等于0
*/
private void setHeadAndPropagate(Node node, int propagate) {
// 记录当前头结点,因为下面setHead之后,头结点会变为当前节点
Node h = head;
//将当前节点设置为头结点,线程置为null
setHead(node);
// 有两种情况是需要执行唤醒操作:
// 1.propagate>0,说明还有资源
// 2.头节点后继节点需要被唤醒(waitStatus<0),不论是老的头结点还是新的头结点
// h.waitStatus < 0 h的waitStatus要么是SIGNAL(-1),要么是PROPAGATE(-3),
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
// 判断后继节点是否存在,如果不存在或该节点是共享模式
// 进行共享模式的释放
if (s == null || s.isShared())
doReleaseShared();
}
}
doReleaseShared
方法
/**
* 从头节点开始,判断头节点后继的状态,来确定后继需不需要唤醒。
*/
private void doReleaseShared() {
for (;;) {
Node h = head;
//只处理头节点和尾节点都存在,且队列内的节点总数超过1个的情况
if (h != null && h != tail) {
int ws = h.waitStatus;
// 如果状态是Node.SIGNAL,就要唤醒节点h后继节点的线程
if (ws == Node.SIGNAL) {
// 将节点h的状态设置成0,如果设置失败,就继续循环,再试一次。
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
// 唤醒节点h后继节点的线程
unparkSuccessor(h);
}
// 如果节点h的状态是0,就设置ws的状态是PROPAGATE,以确保在释放资源时能够继续通知后继节点。
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
// 在循环过程中,为了防止在上述操作过程中新添加了节点的情况,
// 通过检查头节点是否改变了,如果改变了就继续循环
//如果没有改变,就跳出循环
if (h == head)
break;
}
}
acquireSharedInterruptibly
方法
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);
}
}
tryAcquireSharedNanos
方法
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquireShared(arg) >= 0 ||
doAcquireSharedNanos(arg, nanosTimeout);
}
doAcquireSharedNanos
方法
private boolean doAcquireSharedNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
//计算超时时间(绝对时间)
final long deadline = System.nanoTime() + nanosTimeout;
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 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);
}
}
释放同步状态
releaseShared
方法
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
//唤醒等待同步状态的线程
doReleaseShared();
return true;
}
return false;
}
tryReleaseShared
方法
/**
* 共享模式下释放同步状态,需由子类实现
*/
protected boolean tryReleaseShared(int arg) {
throw new UnsupportedOperationException();
}
示例
原始同步队列如下,假设现在线程
t0
共享模式下获取成功获取同步状态。
此时
t0
任务执行完毕调用releaseShared
释放同步状态,假设调用tryReleaseShared
成功,那么接着调用doReleaseShared
,因为head
节点ws
为SIGNAL(-1)
,所以调用unparkSuccessor
唤醒head
节点的后继节点node1
对应的线程t1
,如下图所示:
t1
唤醒后继续执行doAcquireShared
中的自旋逻辑,t1
如果调用tryAcquireShared
成功并且返回值大于等于0,就会继续唤醒其后继节点,于是执行setHeadAndPropagate
,将node1
节点设置为头结点,如下图所示:
此时线程
t1
也调用doReleaseShared
准备唤醒其后继节点,别忘了,线程t0
也有可能还没执行完doReleaseShared
,此时如果线程t0
正在调用:
if (h == head)
break;
结果发现头结点已经变为
node1
,于是也不会退出循环,而是去尝试唤醒新的头结点的后继节点,执行compareAndSetWaitStatus
。
如果在这个时候,线程t1
也刚好执行compareAndSetWaitStatus
,那么必然只有一个线程会调用成功。
类似的场景还有:假设上面线程
t1
执行成功,那么会唤醒node2
对应的线程t2
,t2如果再执行doReleaseShared
可能还是会和t1
撞车(理由同上)。
**好处:**加快唤醒后继节点的速度,减少线程park
时间,从而提高吞吐量。
再来思考另一个问题,
doReleaseShared
中的下面的语句块什么时候执行?
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
首先一个前提条件
if (h != null && h != tail)
,即:队列中至少两个节点。
然后,我们需要知道ws
什么时候为0?
两种情况:一种是上面分析的
compareAndSetWaitStatus(h, Node.SIGNAL, 0)
,但是这样的话就不会进入else if
语句块中。还有一种情况是当前队列最后一个节点成了头结点。因为每次新的节点入队,都会把前驱节点的waitStatus
置为SIGNAL
,只有尾节点是初始值0。
其次,
compareAndSetWaitStatus(h, 0, Node.PROPAGATE)这个操作什么时候会失败?
**
既然这个操作失败,在执行这个操作的瞬间,ws
不为0了,说明有新的节点入队,ws的值被改为了SIGNAL
,此时,我们调用continue
在下次循环中将刚刚入队准备挂起的线程唤醒。
综上,得出的场景是:
doReleaseShared
在自选的过程中,同步队列只剩一个尾节点,在还未进入if (h != null && h != tail)
之前,刚好有一个
新节点入队,但是只执行完addWaiter
,还未走后续流程,所以能够进入if (h != null && h != tail)
块内,但是此时head节点的ws
为0,所以走到了else if
块,就在执行compareAndSetWaitStatus(h, 0, Node.PROPAGATE)
的时候,新加入节点的线程shouldParkAfterFailedAcquire
执行完毕(执行两次,第一次设置状态返回false,第二次返回true),将头结点的waitStatus
置为SIGNAL
,所以compareAndSetWaitStatus
失败,重新回到for循环中继续唤醒head的后继节点,也就是新加入的节点。
所以这在一定程度上也能够加快唤醒后继节点的速度,减少线程
park
时间
。
问题:还是上面那个场景,如果新加入的节点对应的线程
shouldParkAfterFailedAcquire
第一次执行完后,尝试获取同步状态成功,也就不会park
,那么head
节点就会unpark
一个未park
的线程,有什么影响?