上一篇:原子操作CAS
一、显式锁
synchronized
关键字是java内置的语言特性,使用synchronized
关键字会隐式的获取锁,在获取锁的线程执行执行完任务或是执行过程中发生异常时会自动释放锁。但为什么还需要Lock呢?
如,目前绝大多数的业务场景基本都是读多写少,如读写比例为10:1,倘若使用synchronized
,若有一个线程在读,则因为其的独占性
,其他的线程都只能等待,造成大量的资源浪费,但Lock
的实现类ReentrantReadWriteLock
读写锁便可以完美的解决这一问题。
又如,synchronized
无法设置超时时间,而如果获取该锁的线程因为I/O请求或是其他原因导致一直无法释放锁,则其他线程便会进入“无限等待”的状态,而Lock
获取锁时可设置超时时间,如果在截止时间之后仍未获取到锁,则返回。
但个人认为如果不使用lock.trylock或者lockInterruptbly()中断获取锁的线程等,尽量使用synchronized
关键字,synchronized因为是一个语言特性,而Lock是一个类,使用就需要创建对象实例必然会比synchronized有所消耗,jdk对synchronized也进行了许多优化。
二、Lock
先看lock的标准用法:
private Lock lock = new ReentrantLock();
private int num= 1;
.............
public void test() {
lock.lock();
//如果中间抛了个异常,unlock就不会执行,因此释放锁必须放在finally中
try{
age++;
}finally {
lock.unlock();
}
}
这里不能将获取锁放在try中,因为如果放在try中,当获取锁时发生了异常,必然会执行unlock()释放锁,而问题是当前并没有获取到锁,又必然会导致程序异常。
Lock的常用方法如下:
-
lock()
获取锁,调用该方法的线程会获取到锁,如果获取到锁,从该方法返回,否则会进行等待。 -
void lockInterruptibly() throws InterruptedException;
该方法支持中断的获取锁。即在获取锁时,如果锁已被其他线程获取,则当前线程只能进入阻塞等待状态,但 是可以调用该线程的interrupt()
方法中断当前线程的等待状态,停止获取锁。 -
boolean tryLock()
该方法实现非阻塞的获取锁,即调用该方法会立即返回,如果获取到,则返回true,如果未获取到则返回false。
官方给出的范例如下:Lock lock = ...; if (lock.tryLock()) { try { //如果获取到锁就处理相关任务 } finally { lock.unlock(); } } else { //未获取到执行其他任务 }}
如在
ReentrantLock
中,直接调用了非公平锁的实现:
获取一次,获取到就返回true,没获取到返回false,而不胡像lock()不停做自旋获取。public boolean tryLock() { return sync.nonfairTryAcquire(1); } final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }
-
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
同3一样,但增加了超时时间。当:
(1)当前线程在超时时间内获取到锁,则直接返回true
(2)当前线程在超时时间内被中断,则抛出中断异常
(3)当前线程在超时时间内未获取到锁,返回false -
void unlock();
释放锁。 -
Condition newCondition();
返回一个Condition实例,该实例与调用其的的锁绑定,调用后,可使用condition的await()
、signal()
等方法对调用该方法的线程进行等待和唤醒,一个Lock对象可以使用多个Condition
三、Condition
synchronized
关键字可通过wait()、notify()实现等待-通知的模式,而Lock也可以实现,但是其是借助Condition接口实现的,通过Lock.newCondition()就可以看出。
与synchronized不同的是,一个Lock对象可以创建多个Condition实例,因此,每个Condition实例可以维护单独的线程,在调用condition.await()和signal()时,只会控制注册在该condition实例上的线程,signalAll()也不会像synchronized调用notifyAll()一样通知所有阻塞等待的线程,只是唤醒所有注册在该实例的线程。
Condition的常用方法:
-
void await() throws InterruptedException;
与此Condition
关联的锁会被原子释放,并且出于线程调度的目的,当前线程被禁用,进入等待状态,直到发生以下四种情况之一:
1.其他一些线程调用此Condition的signal()
或signalAll()
方法,而当前线程恰好被选择为要唤醒的线程
2.其他线程调用interrupt()
方法中断当前线程
3.发生虚假唤醒
如果 当前线程被唤醒,从await()方法返回,则意味着当前线程已经获取了该Condition对象绑定的锁。 -
void awaitUninterruptibly();
同await()一样,但是不响应中断。即如果当前线程进入此方法时已设置其中断状态,或者在等待时其他线程调用interrupt()
方法,它仍继续等待直到收到signal()或signalAll()。当它最终从该方法返回时,其中断状态仍将被设置。 -
long awaitNanos(long nanosTimeout) throws InterruptedException;
当前线程进入等待状态直到被通知、中断或超出超时时间。
此方法返回剩余时间。因此如果返回小于等于0的数,说明已经超时。 -
boolean await(long time, TimeUnit unit) throws InterruptedException;
同上,但如果超时返回false,否则返回true -
boolean awaitUntil(Date deadline) throws InterruptedException;
当前线程进入等待状态直到被通知、中断或超出截止时间。超时返回false,否则返回true。 -
void signal();
唤醒一个等待队列中等待时间最长的线程。
如果有线程在该Condition下等待,则选择一个线程唤醒。该线程从await()
返回前,必须重新获取Conditon绑定的锁。 -
void signalAll();
唤醒所有在该Condition上等待的线程。同样需要先获取绑定的锁。
Condition简单使用:
package com.wml.test4.conditon;
/**
* @author Wang
* @date 2020/2/117:27
*/
public class ConditionDemo {
private static Delivery delivery = new Delivery("wml", "南京路1号", "德基广场三层", 1000);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 3; i++) {
new Thread(() -> {
delivery.sms300();
}, "distance").start();
}
for (int i = 0; i < 3; i++) {
new Thread(() -> {
delivery.smsArrived();
}, "address").start();
}
System.out.println("配送中");
Thread.sleep(3000L);
delivery.arive300();
Thread.sleep(1000L);
delivery.completeDelivery();
}
}
//配送类
class Delivery {
private Lock lock = new ReentrantLock();
//使用lock创建一个用于实现距离的等待通知的condition
private Condition distConditon = lock.newCondition();
//使用lock创建一个用于实现地点的等待通知的condition
private Condition addrCondition = lock.newCondition();
/**
* 收餐人
*/
private String recipient;
/**
* 配送地址
*/
private String address;
/**
* 当前位置
*/
private String currentAddress;
/**
* 距离
*/
private int distance;
public Delivery(String recipient, String address, String currentAddress, int distance) {
this.recipient = recipient;
this.address = address;
this.currentAddress = currentAddress;
this.distance = distance;
}
/**
* 距离送餐地点还有300米,通知订餐人到指定地点取餐
*/
public void arive300() {
lock.lock();
try {
this.distance = 300;
//到达300米,满足条件,发送通知,唤醒等待线程
distConditon.signal();
} finally {
lock.unlock();
}
}
/**
* 完成配送,更新距离和位置,唤醒线程发送相关通知
*/
public void completeDelivery() {
lock.lock();
try {
this.distance = 0;
this.currentAddress = this.address;
//到达目的地,满足条件,唤醒当前condition实例的等待线程
addrCondition.signal();
} finally {
lock.unlock();
}
}
/**
* 到达300米处发送通知
*/
public void sms300() {
lock.lock();
try {
while (this.distance > 300) {
try {
//阻塞等待到达目的地300米处的条件来唤醒
distConditon.await();
System.out.println("到达300米处,distConditon执行signal唤醒了" + Thread.currentThread().getName() + "线程");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} finally {
lock.unlock();
}
System.out.println("亲爱的" + this.recipient + "先生/女士,您的外卖还有300米送达");
}
//订餐送达目的地
public void smsArrived() {
lock.lock();
try {
try {
while (!this.currentAddress.equals(this.address)) {
//等待到达目的地条件的唤醒
addrCondition.await();
System.out.println("addressCOndition的" + Thread.currentThread().getName() + "线程被唤醒");
System.out.println("订餐已送至目的地:" + this.address);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
lock.unlock();
}
System.out.println("完成配送");
}
}
结果:
配送中
到达300米处,distConditon执行signal唤醒了distance线程
亲爱的wml先生/女士,您的外卖还有300米送达
addressCOndition的address线程被唤醒
订餐已送至目的地:南京路1号
完成配送
该例子使用Lock对象创建了一个距离condition和地址condition,外派配送至距离送餐点300米和送达到指定地点时都需要发送通知,发送通知需要分别调用距离和地址condition的await()方法等待,当到达300米和指定地点时再分别调用对应的condition.signal()唤醒一个等待中的线程。
四、LockSupport
在将Lock的实现类前,需要先将AQS,但是了解AQS又需要先了解LockSupport。
LockSupport 定义了一组以 park 开头的方法用来阻塞当前线程,以及
unpark(Thread thread)方法来唤醒一个被阻塞的线程。
LockSupport类无法被实例化(从以下源码可看出,只有一个私有的构造函数)且所有方法都是静态的,这些方法提供了最基本的线程阻塞和唤醒功能。
public class LockSupport {
private LockSupport() {}
}
再来看看LockSupport的成员变量:
private static final sun.misc.Unsafe UNSAFE;
//挂起线程对象的偏移地址,对应Thread类的parkBlocker字段
private static final long parkBlockerOffset;
private static final long SEED;
private static final long PROBE;
private static final long SECONDARY;
对应的赋值在静态代码块中:
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class<?> tk = Thread.class;
parkBlockerOffset = UNSAFE.objectFieldOffset
(tk.getDeclaredField("parkBlocker"));
SEED = UNSAFE.objectFieldOffset
(tk.getDeclaredField("threadLocalRandomSeed"));
PROBE = UNSAFE.objectFieldOffset
(tk.getDeclaredField("threadLocalRandomProbe"));
SECONDARY = UNSAFE.objectFieldOffset
(tk.getDeclaredField("threadLocalRandomSecondarySeed"));
} catch (Exception ex) { throw new Error(ex); }
}
可以看到都是先获取Thread
类下的对应的字段,如parkBlocker
,然后通过UNFAFE的objectFieldOffset()
方法获取对应字段的偏移量(某字段在其类中的内存偏移量是始终相同的)
再来看看LockSupport的静态方法:
说在前面:LockSupport的park()和unpark()底层在维护一个许可证,park相当于消费者,unpark相当于生产者(生产一个许可证),park在没被中断或其他原因导致停止等待的情况下,必须消费一个许可证才可以继续执行
1.park
相关方法:
public static void park() {
UNSAFE.park(false, 0L);
}
其底层调用的UNSAFE.park(boolean isAbsolute, long time)
方法,其底层原理解析可参考此文章:https://juejin.im/post/5bdc1142e51d45052c6fede7#heading-1,简单的讲,就是其底层维护了一个volatile int _counter
变量,该变量就相当于许可证,park()方法就是将其从1改为0消费掉。其会首先尝试获取许可,如果没有获取到,如果获取到则说明有线程调用unpark()
释放了一个许可,再进行以下情况的判断:
- 当前线程被中断,直接返回
- 当前线程到期,直接返回,具体可分为(以下情况发生中断或出现未知原因也都会返回):
a.isAbsolute
为true,如果time
小于等于0,直接返回;如果大于0,进行粗精度超时计算,超时时间内未获取到许可则返回
b.isAbsolute
为false,time
为0L,不进行超时计算,一直等待unpark释放许可
c…isAbsolute
为false,time
大于0,进行细精度超时计算,如果在time时间内仍未获取到许可,则直接返回
超时相关方法:
public static void parkUntil(long deadline) {
UNSAFE.park(true, deadline);
}
public static void parkNanos(long nanos) {
if (nanos > 0)
UNSAFE.park(false, nanos);
}
2. 带有blocker
的park
相关方法:
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, 0L);
setBlocker(t, null);
}
该方法也是用于阻塞当前线程,其中blocker是用来标识当前线程在等待的对象(阻塞对象),用于问题的排查和系统监控。
setBlocker()
方法如下:
private static void setBlocker(Thread t, Object arg) {
UNSAFE.putObject(t, parkBlockerOffset, arg);
}
可以看到,该方法调用了UNSAFE.putObject()
方法,实现将传入的blocker
,(这里是arg),赋值给当前线程的parkBlocker
对应偏移量下的数据。
回过头看park(Object blocker)
方法,其在调用UNSAFE.park()前设置了blocker,在被唤醒后又调用了setBlocker
将对应位置的blocker清空。因此当前线程在阻塞前,我们仍可以获取到当前的blocker。
获取Blocker
方法如下:
public static Object getBlocker(Thread t) {
if (t == null)
throw new NullPointerException();
return UNSAFE.getObjectVolatile(t, parkBlockerOffset);
}
带blocker的超时的park()方法如下:
public static void parkNanos(Object blocker, long nanos) {
if (nanos > 0) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, nanos);
setBlocker(t, null);
}
}
public static void parkUntil(Object blocker, long deadline) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(true, deadline);
setBlocker(t, null);
}
3.unpark
相关方法:
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
唤醒一个被阻塞的线程。底层就是将_counter
变量设为1,如果_counter本来是0,则会唤醒在等待的线程。
unpark
可以在park
之前使用,这样调用park
时发现_counter
为1,则可以直接消费使用;
unpark()
可以调用多次,但_counter
值最多为1,不会累加,因此连续调用两次park()
只有第一次调用时可以继续执行,第二次就会等待。
下面写一个小例子验证上面的说法:
public class LockSupportDemo {
/**
* 测试blocker在对应线程被唤醒后清空
*/
public static void testBlocker(){
Thread threadA = new Thread(()->{
System.out.println("ThreadA被阻塞前");
LockSupport.park("threadA_Blocker");
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
threadA.start();
new Thread(()->{
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
String blocker = (String) LockSupport.getBlocker(threadA);
System.out.println("线程A即将被唤醒");
System.out.println("获取到线程A的blocker:"+blocker);
//释放线程A
LockSupport.unpark(threadA);
System.out.println("线程A被唤醒了");
System.out.println("再次获取线程A的blocker");
String blocker2=(String)LockSupport.getBlocker(threadA);
System.out.println("获取到线程A的blocker:"+blocker2);
}).start();
}
public static void main(String[] args) throws InterruptedException {
//testBlocker();
Thread thread = new Thread(() -> {
unparkFirst();
});
thread.start();
new Thread(()->{
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("第三次执行unpark");
LockSupport.unpark(thread);
}).start();
}
/**
* 测试先执行unpark,再park可继续执行,但是执行两次unpark,也只能执行一次park,第二次park被阻塞
*/
public static Thread unparkFirst(){
Thread curThread = Thread.currentThread();
LockSupport.unpark(curThread);
LockSupport.unpark(curThread);
System.out.println("执行两次unpark后");
LockSupport.park();
System.out.println("第一次执行park后");
LockSupport.park();
System.out.println("第二次执行park后");
return curThread;
}
}
执行testBlocker()
结果:
ThreadA被阻塞前
线程A即将被唤醒
获取到线程A的blocker:threadA_Blocker
线程A被唤醒了
再次获取线程A的blocker
获取到线程A的blocker:null
发现在线程A被唤醒后,对应的blocker为空
执行unparkFirst()
结果:
执行两次unpark后
第一次执行park后
第三次执行unpark
第二次执行park后
发现第二次执行park后,线程被阻塞等待,只有再次执行unpark后,后面的代码才能继续执行,因此park和unpark应成对出现,否则线程会一直等待下去。
五、AQS
讲完了LockerSupport,接下来就可以讲解AQS了。
5.1 CLH队列锁
CLH 队列锁是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程
仅仅在本地变量上自旋,它不断轮询前驱的状态,假设发现前驱释放了锁就结束自旋,获取对应的锁。
CLH队列锁由一个前驱节点pre,一个当前节点curNode,一个tail尾节点和一个locked位构成
5.1.1 获取锁
若当前线程A需要获取锁,则线程A创建一个新的QNode,先将其locked
位设为true
,表示需要获取锁,在调用tail.getAndSet(curNode)
,CAS的方式将自己设为尾部(这样后面再有需要获取锁的就必须在线程A后排队),同时该方法返回原来的尾节点,让自己的前驱节点指向原尾节点(即指向前一个线程的node节点,如果是第一个获取锁的线程,则pre指向null,因为tail初始化为null),最后循环访问原尾节点的locked位,当其为false时,停止循环,即停止自旋,获得到锁。
示意图如下:
这样只要前面的线程使用锁结束,locked位变为false,后面排队的线程就可以获取到锁。
5.1.2 释放锁
释放锁比较简单,直接将当前节点的locked位设为false,然后让当前节点设为前驱节点,代表出队。
简单实现:
/**
* @author Wang
* @date 2020/2/216:19
*/
public class CLH {
private final AtomicReference<QNode> tail= new AtomicReference<>(new QNode());;
private final ThreadLocal<QNode> pre;
private final ThreadLocal<QNode> curNode;
private static class QNode {
volatile boolean locked = false;
}
public CLH() {
curNode = ThreadLocal.withInitial(() -> new QNode());
pre = ThreadLocal.withInitial(() -> null);
}
public void lock() {
QNode node = curNode.get();
node.locked = true;
QNode pred = tail.getAndSet(node);
pre.set(pred);
while (pred.locked) {}
}
public void unlock() {
QNode qnode = curNode.get();
qnode.locked = false;
curNode.set(pre.get());
}
}
5.2 AQS
AQS即队列同步器,是用来构建锁和其他同步组件的基础框架,如即将要将的Lock的几个实现类,ReentrantLock
、ReentrantReadWriteLock
以及前面文章提到的CountDownLatch
、Semaphore
等都是基于AQS框架实现的,当然我们也可以借助AQS实现自己的同步器。
使用方式:
子类通过继承AQS,实现AQS提供的抽象方法来管理同步状态,在AQS中维护了一个int类型的state代表该状态,并提供了getState()
、setState(int newState)
和 compareAndSetState(int expect,int update)
等方法实现对共享资源state的获取和释放;
而该子类推荐使用静态内部类的方式实现,AQS只是定义了若干同步状态获取和释放的方法提供自定义同步组件使用,其支持独占式和共享式两种方式获取同步状态。
同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。锁和同步器很好地隔离了使用者和实现者所需关注的领域。
实现者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步
组件的实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者重写的方法。
模板方法
AQS的设计是基于模板方法模式的。那么什么是模板方法呢?简单的说就是在方法中只定义了该方法的骨架,但是具体的实现步骤放在了其子类中。如Spring中的各种各样的Template模板。
如还不懂什么是模板方法,请看以下例子:
1.定义一个改卷子的抽象类:
public abstract class AbstractMarking {
//改卷子
protected abstract void check();
//计算总分
protected abstract void compute();
//划分等第
protected abstract void rank();
/**
* 批卷的模板方法
* 提供了以上三个抽象方法,具体实现在子类
*/
public final void marking(){
check();
compute();
if (shouldRank()){
rank();
}
}
/**
* 是否要打等级(有的科目根据分数要划分等级,有的则按分数评)
* @return
*/
protected boolean shouldRank(){
return false;
}
}
2.分别定义数学、英语和实训类实现改卷子的抽象类
public class English extends AbstractMarking {
@Override
protected void check() {
System.out.println("改英语卷子");
}
@Override
protected void compute() {
System.out.println("计算英语分数");
}
@Override
protected void rank() {
System.out.println("不需打英语");
}
}
public class Practical extends AbstractMarking {
@Override
protected void check() {
System.out.println("改实训成果");
}
@Override
protected void compute() {
System.out.println("计算实训分数");
}
@Override
protected void rank() {
System.out.println("打实训等级");
}
@Override
protected boolean shouldRank() {
return true;
}
}
调用:
public class Main {
public static void main(String[] args) {
AbstractMarking englishMarking=new English();
englishMarking.marking();
AbstractMarking mathMarking=new Math();
mathMarking.marking();
AbstractMarking pracMarking=new Practical();
pracMarking.marking();
}
}
结果:
改英语卷子
计算英语分数
改高数卷子
计算高数分数
改实训成果
计算实训分数
打实训等级
AQS中的模板方法:
实现自定义同步组件时,将会调用同步器提供的模板方法。
独占模式:
-
void acquire(int arg)
独占式获取同步状态,如果当前线程获取同步状态成功,则由该方法返回,否则进入同步队列等待,该方法会调用重写的tryAcquire(int arg)方法 -
void acquireInterruptibly(int arg)
同上,但相应中断,当前线程未获取到同步状态而进入同步队列,但如果当前线程被中断,则该方法会抛出InterruptedException
异常并返回。 -
boolean tryAcquireNanos(int arg, long nanosTimeout)
同2,但增加了超时时间,如果当前线程在超时时间内未获得到同步状态,则返回false,反之返回true -
protected boolean tryAcquire(int arg)以及tryRelease(int arg)
(可重写)
tryAcquire独占式获取同步状态,实现该方法需要查询当前状态并判断同步转阿根廷是否符合预期,然后再进行CAS设置同步状态。获取成功返回true,反之返回false。
tryRelease度展示释放同步状态,等待获取同步状态的线程将有机会获取同步状态 -
boolean release(int arg)
独占式地释放同步状态,该方法会在释放同步状态后,将同步队列中第一个节点包含的线程唤醒。
共享模式:
提供了与独占模式对应的带有Shared的共享式相关的方法。
以
void acquireShared(int arg)
为例,共享式的获取同步状态,如果未获取到,则将当前线程加入同步队列等待,与独占式不同的是该方法允许同一时刻多个线程获取到同步状态。
AQS中的节点和同步队列数据结构
在介绍CLH锁时候已经提到了,AQS是基于CLH的变种实现,对应的就是静态内部类Node
,定义如下:
先看Node的两个等待模式和几个状态:
static final class Node {
//标明线程以共享模式等待锁,如读锁ReadLock
static final Node SHARED = new Node();
//标明线程以独占模式等待锁,如可重入锁ReetrantLock(即一把锁一次只能由一个线程持有,而共享锁允许一把锁一次被多个线程持有)
static final Node EXCLUSIVE = null;
/**
*以下几个是线程在队列中的状态
*/
// 表示线程获取锁的请求已经 取消
static final int CANCELLED = 1;
// 后驱节点入队等待后会更新前驱节点的状态为SIGNAL,等待前驱节点释放同步状态,前驱节点释放后会唤醒后驱节点
static final int SIGNAL = -1;
//表示节点在等待队列中,当其他线程调用的condition.signal()后,CONDITION
static final int CONDITION = -2;
//在共享模式下,前驱节点在唤醒后驱节点的同时,也会无条件的唤醒后驱节点的后驱节点,一直传递下去
static final int PROPAGATE = -3;
除此之外。还有一个值为0的状态,代表初始化Node对象的默认值。
从以上变量可看出,当值为负的时候,表示结点等待状态有效,而正值表示结点已被取消。
因此可通过判断状态的值是否小于0来判定其是否正常。
成员变量:
//线程在队列中的等待状态, 值为以上几个
volatile int waitStatus;
//当前节点的前驱节点
volatile Node prev;
//当前节点的后驱节点
volatile Node next;
//当前节点对应的线程
volatile Thread thread;
//Node作为同步队列节点时,nextWaiter有两个值:EXCLUSIVE、SHARED标识当前节点是独占模式还是共享模式
//Node作为等待队列节点时,nextWaiter表示当前节点的后驱节点。
Node nextWaiter;
另外,AQS还提供了头节点和尾节点的引用。
private transient volatile Node head;
private transient volatile Node tail;
但这里的head节点不保存线程信息。如下:
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
head节点是获取同步状态成功的节点,head节点的线程在释放同步状态时,会唤醒其后驱节点,而后驱节点将会在获取同步状态成功时将自己设置为head节点。
设置首节点每次都是通过获取同步状态成功的那个线程实现的,因为一次只有一个线程能够成功获取到同步状态,所以设置头节点的方法并不需要使用 CAS ,只需要将头节点设置成为原头节点的后驱节点并断开与原头节点的连接。
独占式同步状态的获取和释放
1.获取
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
1.首先调用自定义同步器实现的 tryAcquire(int arg)方法尝试获取锁,该方法需要保证线程安全的获取同步状态,若成功,则返回true,该方法直接返回,若失败,则返回false,继续执行后面的代码。
2.如果同步状态获取失败,则构造同步节点并通过addWaiter(Node node)
方法将该节点加入到同步队列的尾部。代码如下:
Node(Thread thread, Node mode) {
this.nextWaiter = mode;
this.thread = thread;
}
private Node addWaiter(Node mode) {
//通过当前线程和mode(Node.EXCLUSIVE)构造一个Node节点,在上面的构造方法中可看出mode被赋值给nextWaiter,也印证了上面成员变量中对nextWaiter的解释
Node node = new Node(Thread.currentThread(), mode);
//获取原来的尾节点
Node pred = tail;
if (pred != null) {
//尾节点不为空,让当前节点的前驱指向tail
node.prev = pred;
//通过CAS更新尾节点
if (compareAndSetTail(pred, node)) {
//CAS成功,原tail节点的后驱指向当前节点,实现双向链表
pred.next = node;
//返回当前节点
return node;
}
}
//如果尾节点为空或CAS失败,则调用enq(node)将当前节点入队,返回node
enq(node);
return node;
}
入队操作如下:
通过一个死循环,不停的CAS设置尾节点
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) {
//尾节点为空,初始化队列,构建一个新的头节点,并让尾节点等于头接地那
if (compareAndSetHead(new Node()))
tail = head;
} else {
//尾节点不为空
node.prev = t;
//CAS设置尾节点
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
即入队时,如果当前线程是第一个加入同步队列,则通过compareAndSetHead(new Node())
初始化队列头
否则,进行自旋不断CAS将节点插入尾节点
3.最后调用 acquireQueued(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();
//只有前驱节点是头节点才可以tryAcquire(具体实现见AQS实现类)
if (p == head && tryAcquire(arg)) {
//如果获取同步状态成功,则将头节点换为当前节点
setHead(node);
p.next = null; // help GC
//成功
failed = false;
//返回中断状态,此时不需要中断
return interrupted;
}
//前驱节点非头节点或获取同步状态失败
if (shouldParkAfterFailedAcquire(p, node) //判断获取同步状态失败后是否需要阻塞
&& parkAndCheckInterrupt())//如果需要阻塞,
interrupted = true;//置中断标志为true
}
} finally {
//如果获取失败,则将当前节点取消
if (failed)
cancelAcquire(node);
}
}
shouldParkAfterFailedAcquire
如下:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//获取前驱节点的状态
int ws = pred.waitStatus;
//如果前驱节点是SIGNAL(前驱节点释放锁后会通知后驱节点),则当前节点可以进行安全等待
if (ws == Node.SIGNAL)
return true;
//ws大于0即处于CANCEL状态
if (ws > 0) {
//循环将当前节点与前驱节点为非CANCEL状态的节点连接,即循环遍历找到有效状态节点
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//处于其他状态时,将前驱节点的状态更新尾SIGNAL,然后返回false,会在acquireQueued中继续下一个循环,直到前驱节点状态为SIGNAL才会继续执行下一个方法
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
可见,该方法只有在前驱节点状态为SIGNAL时才会返回true,可以进行安全等待。
即: 第一次循环将CANCLED状态的节点删除,如果没有被取消的节点,则将pred设为SIGNAL
第二次循环如果是SIGNAL的话,就直接返回true
接着会调用parkAndCheckInterrupt()
,阻塞当前线程,并返回中断状态
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
取消节点流程如下:
private void cancelAcquire(Node node) {
// 当前节点是空的,直接返回
if (node == null)
return;
// 先让当前节点的线程置空
node.thread = null;
// 获取当前节点的前驱节点
Node pred = node.prev;
while (pred.waitStatus > 0)
// 如果前驱节点被取消,则向前循环直到找到一个非CANCELD的节点
node.prev = pred = pred.prev;
// 获取当前pred的后驱节点(后面会用到)
Node predNext = pred.next;
// 将当前节点的状态改为取消
node.waitStatus = Node.CANCELLED;
// 如果被取消的节点是尾节点
if (node == tail
&& compareAndSetTail(node, pred)) { //将尾节点更新尾pred(即从后往前第一个非CANCELED的节点)
compareAndSetNext(pred, predNext, null);// 让新的尾节点的next为null
} else {
//被取消的节点为中间节点
int ws;
// 1.如果取消节点的前驱节点为头节点,则进入unparkSuccessor(node);唤醒后驱节点
//2. 如果其前驱节点不为头节点
//2.1 如果pred的状态为SIGNAL或修改pred的状态为SIGNAL成功则将前驱节点的next设为被取消节点的next,
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 {
//3 唤醒后驱节点
unparkSuccessor(node);
}
node.next = node; // help GC
}
流程总结:
- 调用
tryAcquire
尝试获取同步状态,如果获取成功则返回,否则进入 2 - 调用
addWaiter
将当前线程构造一个新节点加入同步等待队列,如果队列未初始化则先初始化,否则直接将新节点加入队尾 - 接着调用
acquireQueued
让加入队列的节点开始自旋,只有该节点的前驱节点为head时才可以尝试获取状态(这里一是因为头节点是成功获取到同步状态的节点,头节点释放同步状态会唤醒后驱节点,所以要加此判断,二是因为队列的FIFO),如果不是头节点或争夺锁失败,则跳过处于CANCEL状态的节点,并将前驱节点更新为SIGNAL,只有前驱节点状态为SIGNAL时,代表后面的节点在等待上一个线程发来的信号,然后该节点才可以进行park等待,返回中断状态 - 如果被中断,则调用
selfInterrupt()
中断当前线程
2.释放
释放同步状态通过release
方法,释放后会唤醒后驱节点,让后驱节点重新尝试获取同步状态
public final boolean release(int arg) {
if (tryRelease(arg)) {///调用模板方法释放同步状态,成功返回true,失败返回false 具体实现在子类
Node h = head;
//头节点不为空且状态非0时唤醒后驱节点
//在每个节点阻塞前,会将前一个接地那的状态改为SIGANAL(-1)
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
看一下子类ReentrantLock
关于tryRelease
的实现:
protected final boolean tryRelease(int releases) {
// 计算当前状态减去releases后的值
int c = getState() - releases;
// 如果当前线程不是独占模式的,则抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
// 可以理解为是否彻底释放
boolean free = false;
// 如果c为0,说明当前线程已经将锁释放完毕,即重入的次数都已释放
if (c == 0) {
// 置为true,表示已彻底释放
free = true;
// 将当前独占线程清空
setExclusiveOwnerThread(null);
}
// 如果自减一后的state不为0,说明尚未释放完毕,返回false
setState(c);
return free;
}
unparkSuccessor
如下:
private void unparkSuccessor(Node node) {
//获取头节点(要释放同步状态的节点)的状态
int ws = node.waitStatus;
if (ws < 0)
//将状态改为0
compareAndSetWaitStatus(node, ws, 0);
//获取后驱节点
Node s = node.next;
//如果后驱节点为空或已经被取消
if (s == null || s.waitStatus > 0) {
//重新置为null
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);
}
流程:
- 先尝试释放,将state减1后,如果减1后state为0,则代表完全释放了锁,会先同过
setExclusiveOwnerThread
将当前线程设为null,再唤醒下一个等待中的节点 - 如果没有到0,说明没有完全释放,存在锁重入,返回false,释放失败
那么有一个问题,为什么要从尾部遍历呢?
从Node类的源码给出如下解释:
首先看一下之前讲的addWaiter
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.prev=pred,将当前节点与前驱节点连接,然后compareAndSetTail(pred, node)
原子更新尾节点后才执行pred.next = node;
将前驱节点的next引用与新的尾节点连接起来,如果在该操作执行前调用了unparkSuccessor
就无法从前往后完全遍历
共享模式同步状态的获取和释放
获取
以共享模式获取同步状态,忽略中断
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
tryAcquireShared(int arg)
模板方法返回值为 int 类型,当返回值大于等于 0 时,表示能够获取到同步状态。
可以简单看一下Semphore中非公平锁对该方法的实现:
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
final int nonfairTryAcquireShared(int acquires) {
//通过一个死循环获取同步状态
for (;;) {
//获取当前同步状态,这里表示当前可用许可数
int available = getState();
//减去本次申请许可数acquires,得到申请成功后的剩余许可数remaining
int remaining = available - acquires;
if (remaining < 0 || //如果remaining小于0,则本次申请没有获取成功,直接返回,后续加入等待队列
//如果remaining>0,则CAS地重新设置状态,更新尾remaining,返回remaining
compareAndSetState(available, remaining))
return remaining;
}
}
回到acquireShared()
,如果获取同步失败,则执行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) {
//只有前驱节点是head节点时才可以尝试获取同步状态,原因前面已讲过
int r = tryAcquireShared(arg);
//返回值大于0代表获取成功
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);
}
}
总体逻辑和独占式的相同,但因是共享式的,所有在获取了同步状态后需要继续唤醒后驱节点,无条件传播下去。
接下来看setHeadAndPropagate
的实现:
private void setHeadAndPropagate(Node node, int propagate) {
//获取旧的头节点后面会用到
Node h = head;
//将当前节点设为新的头节点
setHead(node);
//propagate > 0 代表还有剩余同步状态
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
来判断,以及PROPAGATE
的意义,另一个博主的这篇文章做了很详细的解释:https://www.cnblogs.com/micrari/p/6937995.html
释放:
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {//尝试释放同步状态
doReleaseShared();
return true;
}
return false;
}
doReleaseShared()
如下:
共享模式下,可能会有多个线程释放同步状态
private void doReleaseShared() {
for (;;) {
Node h = head;
//头节点非空且不等于尾节点
if (h != null && h != tail) {
//获取头节点状态
int ws = h.waitStatus;
//如果状态为SIGNAL,则CAS将其状态改为0,如果CAS失败,则一直循环,如果成功则唤醒后驱节点
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
unparkSuccessor(h);
}
else if (ws == 0 && //如果状态为0,则改为PROPAGATE,以确保释放后继续传播
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
//这里,如果头节点改变了,则继续循环,否则直接break
if (h == head)
break;
}
}
AQS中的条件队列ConditionObject
前面已经介绍了Condition
接口,而ConditionObject
是Condition
的一个实现类,为单向链表。
该条件队列的节点也是使用了内部类Node
,可以在不满足某个条件的时候挂起线程等待,直到满足某个条件的时候在唤醒线程。
下面看一下该类的成员变量:
//等待队列的头节点
private transient Node firstWaiter;
//等待队列的尾节点
private transient Node lastWaiter;
//在退出等待时重新中断
private static final int REINTERRUPT = 1;
//在退出等待时抛出InterruptedException
private static final int THROW_IE = -1;
一个 Condition 包含一个等待队列。
Condition.await()方法,将会以当前线程构造节点,并将节点从尾部加入等待队列。
newCondition
看一下ReentrantLock中的该方法:
public Condition newCondition() {
return sync.newCondition();
}
final ConditionObject newCondition() {
return new ConditionObject();
}
这里就是AQS中的ConditionObject
类
1.先看await()
方法:
public final void await() throws InterruptedException {
//1,如果中断,直接抛出异常
if (Thread.interrupted())
throw new InterruptedException();
//2.将当前线程加入条件等待队列
Node node = addConditionWaiter();
//3.释放当前锁,并唤醒后驱节点,返回释放前的同步状态(因为是可重入的锁)
int savedState = fullyRelease(node);
int interruptMode = 0;
//4.循环判断当前node是否已经转移到AQS队列中,直到成功转移到AQS队列结束循环
while (!isOnSyncQueue(node)) {
//4.1 阻塞当前线程,直到被unpark或中断
LockSupport.park(this);
//条件满足,尝试获取锁
//4.2如果发生了中断,则要检查中断,并检查节点已是否经加入到同步队列,如已加入,则break
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
if (acquireQueued(node, savedState) //5.死循环重新获取同步状态(刚刚释放了多少这里就获取多少),返回false获取成功,返回true获取失败
&& interruptMode != THROW_IE) //6.如果被signal后发生中断
interruptMode = REINTERRUPT;//将中断模式改为REINTERRUPT
if (node.nextWaiter != null)
//7. 删除取消的后驱节点
unlinkCancelledWaiters();
if (interruptMode != 0)
//8.如果线程中断了,则抛出异常,如果为REINTERRUPT则中断当前线程
reportInterruptAfterWait(interruptMode);
}
添加到等待队列的方法addConditionWaiter()
:
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
private Node addConditionWaiter() {
//获取尾节点
Node t = lastWaiter;
//如果尾节点状态为CANCEL,则移除它
if (t != null && t.waitStatus != Node.CONDITION) {
//从头开始清除CANCEL的节点,将所有CONDITION的节点连接起来
unlinkCancelledWaiters();
//重新获取尾节点lastWaiter
t = lastWaiter;
}
//构建一个新的condition类型Node加入条件等待队列
Node node = new Node(Thread.currentThread(), Node.CONDITION);
//如果尾节点此时为空,则重新初始化首尾相同的队列
if (t == null)
firstWaiter = node;
else
//将尾节点的后驱引用指向当前节点
t.nextWaiter = node;
//更新尾节点
lastWaiter = node;
return node;
}
上述节点引用更新的过程并没有使用 CAS 保证,原因在于调用 await()方法的线程必定是获取了锁的线程,也就是说该过程是由锁来保证线程安全的。
unlinkCancelledWaiters()
源码:
private void unlinkCancelledWaiters() {
//获取首节点
Node t = firstWaiter;
//保存当前节点的前驱节点的引用
Node trail = null;
while (t != null) {
//获取后驱节点
Node next = t.nextWaiter;
//如果当前节点的状态非CONDITION,则移除并将整个链表连接起来
if (t.waitStatus != Node.CONDITION) {
//断开当前节点的后驱引用
t.nextWaiter = null;
if (trail == null)
//首节点指向下当前节点的后驱节点
firstWaiter = next;
else
//前驱节点存在,则让前驱节点直接跨过当前节点指向当前节点的后驱节点
trail.nextWaiter = next;
if (next == null)
lastWaiter = trail;
}
else
//当前节点状态为CONDITION,则让trail指向当前节点,当前节点顺为下个节点
trail = t;
t = next;
}
}
彻底释放锁fullyRelease(Node node)
即无论多少次重入,通通清零
final int fullyRelease(Node node) {
boolean failed = true;
try {
//获取当前同步状态,也是重入的次数
int savedState = getState();
//调用release方法释放并唤醒下一个同步队列的线程
if (release(savedState)) {
failed = false;
return savedState;
} else {
//如果释放失败,则抛出异常
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
//如果失败,则取消当前节点
node.waitStatus = Node.CANCELLED;
}
}
isOnSyncQueue(Node node):
如果该节点之前在条件等待队列,但现在在同步队列中,则返回true;如果不在同步队列返回false
//node为当前尾节点
final boolean isOnSyncQueue(Node node) {
//node的状态为CONDITION或node的前驱为空,则说明不在同步队列中
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
//如果当前node 有后继节点,则说明其在同步队列中
if (node.next != null)
return true;
//到此方法就说明当前尾节点前驱不为空,且后驱为空,此时可以认为node正处于放入同步队列enq方法中的compareAndSetTail(t, node)操作中,前面已经分析过,此时已经连接好了pre但没有连接next,而这个CAS操作有可能失败,所以通过findNodeFromTail再尝试一次判断,看看当前节点在没有AQS队列中
return findNodeFromTail(node);
}
//该方法中通过一个死循环从同步队列的尾到前遍历寻找当前节点,如果找到返回ture,否则返回false
private boolean findNodeFromTail(Node node) {
Node t = tail;
for (;;) {
if (t == node)
return true;
if (t == null)
return false;
t = t.prev;
}
}
checkInterruptWhileWaiting(Node node)
检查是否中断,如果在signal之前中断,则返回THROW_IE
,如果在signal之后,则返回REINTERRUPT
,如果没有中断,则为0。
private int checkInterruptWhileWaiting(Node node) {
return Thread.interrupted() ?
//如果发生了中断需要调用transferAfterCancelledWait保证中断的线程已经加入到同步队列
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
0;
}
transferAfterCancelledWait(Node node)
保证中断的线程已经加入到同步队列,判断中断的时候,是否有signal方法的调用,
如果返回false
表示在中断前被signal,之后checkInterruptWhileWaiting
返回REINTERRUPT
重新中断;
如果返回true
表示在中断后被signal,之后checkInterruptWhileWaiting
返回THROW_IE
抛出中断异常。
final boolean transferAfterCancelledWait(Node node) {
//将当前节点的状态从CONDITION改为0(从这也可以看出isOnSyncQueue方法中通过判断状态是否为CONDITION来判断是否被转移到同步队列)
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
//如果CAS成功,说明没有被signal,如果被signal,其状态会改为SIGNAL(后边会讲到),则加入同步队列
enq(node);
return true;
}
//如果状态CAS失败,即node状态不为CONDITION,说明已经被signal或被中断,但不能保证先后顺序,通过while循环等待node已经转移到同步队列
while (!isOnSyncQueue(node))
Thread.yield();
//返回false重新中断
return false;
}
await
的带超时时间的其他几个方法同这个基本一样,只是加了超时时间,具体作用看上面的Condition
的介绍
2.再看signal相关
将等待时间最长的线程(如果存在),从条件等待队列移至拥有锁的同步等待队列。
执行signal
,一定是获取到了锁
public final void signal() {
//1.首先判断当前线程是不是独占模式,如果不是则抛出异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
doSignal(Node first)
会从从首节点开始遍历,把第一个非空、没取消的节点转移到同步队列
private void doSignal(Node first) {
do {
//删除first节点
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&//将该节点转移到同步队列
(first = firstWaiter) != null);
}
transferForSignal(Node node)
会将节点从条件队列转移到AQS队列
final boolean transferForSignal(Node node) {
//尝试将Node的状态从CONDITION改为0,
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
//如果CAS失败,说明节点在signal前被取消,返回false,转移失败
return false;
//CAS成功,则将该节点加入AQS队列尾部,并返回之前的tail
Node p = enq(node);
//获取之前的tail的状态
int ws = p.waitStatus;
if (ws > 0 //>0,即之前的tail被取消, 则直返回唤醒当前节点的线程
|| !compareAndSetWaitStatus(p, ws, Node.SIGNAL))//如果没有被取消,则将原tail节点状态设为SIGNAL,如果失败了则直接唤醒当前线程,如果成功了则前驱节点被设为SIGNAL,返回true,测试节点仍然是阻塞的,即signal并不会真正唤醒一个节点
LockSupport.unpark(node.thread);//唤醒当前节点,当前线程可从await()中的park()返回
return true;
}
另外还有signalAll()方法。实现将Condition队列上的所有节点转移到AQS同步队列去竞争锁(独占)。
总结:
一个同步器拥有一个同步队列(双向)和多个等待队列(单向)。调用 await()相关的方法,会让当前线程进入等待队列
并释放锁
,同时线程进入等待
状态。从 await()方法返回时,当前线程一定获取了 Condition 绑定的锁。
1.调用 await()
相关方法时,将同步队列的首节点的线程获取到了锁,将通过addConditionWaiter()
方法把当前线程构造成一个新的节点并加入等待队列中,然后释放同步状态(当前线程占有的锁),唤醒同步队列中的后驱节点,随后当前线程会进入等待状态,等待条件满足(unpark())
2.等待队列中的节点通过其他线程调用Condition.signal()
方法被唤醒后,被唤醒的线程开始尝试获取同步状态。如果是通过中断唤醒,则会抛出InterruptedException,如果不是,将中断模式设为REINTERRUPT
,并将被取消的后驱节点清除。
3.调用signal()
方法,会唤醒在等待队列中等待时间最长的节点(首节点),在唤醒节点之前,会将该节点转移到同步队列中。但当前线程必须是获取了锁 。
4.接着获取等待队列的首节点,将其全地转移到同步队列后并使用 LockSupport .unpark
唤醒该节点中的线程。
5.被唤醒的线程就会从 await()
方法中的 while (!isOnSyncQueue(node))
循环中退出。(isOnSyncQueue(Node node)方法返回 true,节点已经在同步队列中),
6.然后调用 acquireQueued()
方法加入到获取同步状态的竞争中。成功获取同步状态(或者说锁)之后,被唤醒的线程将从先前调用的 await()方法返回,此时该线程已经成功地获取了锁。
- await()和signal()执行时一定是获得到锁了
- signal()把第一个非空、没取消的节点转移到同步队列,但是signal不会真正唤醒一个节点,当前面节点没有被取消,且CAS更新为SIGNAL失败了,则仅仅是将节点转移到同步队列,真正唤醒节点是在finally中的
unlock()
方法中进行。唤醒后wait()
方法的while循环返回,然后继续执行,重新获取锁。
waitStatus的变化:
- 在条件队列中new的节点为
CONDITION
- 被signal后,移动到AQS队列中,该状态变为初始默认状态0
- 同步队列中,入队等待的节点会将其前驱节点更新为SIGNAL
- 被取消的节点都是CENCELLED
- 共享模式下,前驱节点在唤醒后驱节点的同时,也会无条件的唤醒后驱节点的后驱节点,一直传递下去
(部分分析可能有瑕疵,还请指正,感谢~)