- 一、Lock
- 二、ReentrantLock
- 三、 ReentrantLock的实现原理?
- 4、ReentrantLock的源码分析
- 五、condition条件队列
- 1. AQS.ConditionObject
- 2. Condition接口
- 3. lock.newCondition
- 4.Condition.await
- 5. condition.addConditionWaiter
- 6.AQS.fullyRelease
- 8. Condition.signal
- 9. Condition.doSignal
- 10. AQS.transferForSignal
- 11. Condition.await
- 12. Condition.checkInterruptWhileWaiting
- 13. AQS.transferAfterCancelledWait
- 14. AQS.reportInterruptAfterWait
- 总结
一、Lock
1. lock实现
lock是一个接口,它定义了获取锁跟释放锁的方法,lock定义成接口说明它是锁的一个规范,具体该怎么实现,由其实现类进行特性。其中ReentrantLock是唯一直接实现了Lock的接口
- ReentrantLock:表示可重入锁 ,可重入锁的含义就是,当某一个线程调用方法获取锁之后,倘若再次调用方法获取锁后,该线程不会被阻塞,而是直接累加计数器
- ReentrantReadWriteLock:可重入读写锁,它实现了ReadWriteLock接口
-
2.lock结构
void lock();//获取锁,获取不到锁则当前线程被阻塞,直到锁被释放void lockInterruptibly() throws InterruptedException;//跟lock方法相似,不过该方法可以 让被阻塞的线程中断唤醒,抛出异常boolean tryLock();//尝试获取锁,获取锁成功返回true,失败返回falseboolean tryLock(long time, TimeUnit unit) throws InterruptedException;//带超时时间的尝试获取锁,超时抛出异常void unlock();//释放锁
二、ReentrantLock
reentrantLock 代表可重入锁,可重入锁的含义指的是当线程A占用锁之后,再次去获取锁的时候,线程A不会被阻塞,而是直接在计数器累加,返回。
1、ReentrantLock使用
三、 ReentrantLock的实现原理?
锁通常是将多线程并行的任务采用某一种机制,让线程变得串行,这样就保证线程的安全,那ReentrantLock是如何实现线程的安全?
需要我们分析ReentrantLock的结构。1、AQS
在ReentrantLock类中,有一个类AbstractQueuedSynchronizer,简称AQS,俗称抽象队列同步器,该类是保证ReentrantLock实现线程安全的重要一个类;ReentrantLock的获取锁,释放锁都需要该类进行支持
2、AQS的功能
AQS从使用角度上来说,分为两种功能:
独占模式:指的是每次只能有一个线程占用锁
-
3、AQS的内部结构
AQS内部维护了一个FIFO先进先出的队列(双向链表),Head永远都指向当前占用锁的线程,Tail指向队尾节点,head.next节点任何时候都有权力去尝试获取锁。每一个Node节点都有前驱节点跟后继节点,这样的结构能够很方便的知道一个节点的前驱跟后继节点,每一个Node节点都其实指的是一个线程,一个由线程包装的Node节点,当线程抢占锁失败后,线程会被封装为Node节点,然后被添加到AQS阻塞队列里,当占用锁的线程释放锁之后,会从AQS阻塞队列里唤醒一个Node节点(唤醒Node对应的线程)。

//头节点,任何时候头节点对应的线程都是当前占用锁的线程private transient volatile Node head;//阻塞队列的尾节点(阻塞队列不包含head节点 head.next -->tail 阻塞队列)private transient volatile Node tail;//表示资源//独占模式下 0 ->未加锁状态 1->已加锁状态private volatile int state;//aqs的父类的属性,独占模式下,表示当前持有锁的线程private transient Thread exclusiveOwnerThread;
1、Node结构
static final class Node {//枚举:共享模式static final Node SHARED = new Node();//枚举:独占模式static final Node EXCLUSIVE = null;//表示当前节点处于 取消 状态static final int CANCELLED = 1;//表示当前节点需要唤醒它的后继节点(SIGNAL其实是后继节点的状态,需要当前节点去喊它)static final int SIGNAL = -1;static final int CONDITION = -2;static final int PROPAGATE = -3;//node状态,可选值(0,SIGNAL(-1),CANCELLED(1),CONDITION(-2),PROPAGATE(-3))//默认waitStatus == 0,waitStatus> 0 取消状态//waitStatus == -1 表示当前节点如果是head节点,释放锁之后,需要唤醒他的后继节点volatile int waitStatus;//因为需要构造fifo队列,所有prev 指向前继节点volatile Node prev;//因为需要构造fifo队列,所有next 指向后继节点volatile Node next;//当前node封装的线程本身volatile Thread thread;//Node nextWaiter;Node() {}Node(Thread thread, Node mode) {this.nextWaiter = mode;this.thread = thread;}Node(Thread thread, int waitStatus) {this.waitStatus = waitStatus;this.thread = thread;}
2、抢占锁失败添加Node入队列
线程抢占锁失败后会将该线程封装为Node节点添加到阻塞队列里,队列会发生变化,其中入队过程分为三步:
新添加的节点要主动做事情,将node.prev前驱节点设置为当前tail节点
使用cas方式将当前tail节点修改为新添加的Node节点,为什么使用cas方式,因为这里可能多线程写入队列
cas成功后,将老的tail节点的next指向当前tail节点,完成双向绑定
3、释放锁唤醒Node
当占用所的线程释放锁之后,需要从AQS阻塞队列里唤醒一个Node,此时阻塞队列的节点也会发生变化:
唤醒Node分为两步
- 将head指向唤醒的Node节点,Node节点的Thread属性置为null,Node的prev置为null
- 将老的head节点的next置为null
4、ReentrantLock的源码分析
在进行分析前,我们还是看下ReentrantLock的结构
ReentrantLock内部有一个Sync类,Sync是一个抽象静态内部类,继承了AQS来实现重入锁的逻辑,子类有FailSync,NoFailSync两个,分别代表公平锁,非公平锁,图形象的说明了ReenTrantLock的实现是靠AQS来实现。
公平锁跟非公平锁的唯一区别在于:非公平锁在获取锁的时候不会考虑阻塞队列是否有等待者,而是一上来就使用cas方式去获取锁,获取锁成功则返回true,失败则尝试去获取锁,在尝试获取锁,无锁前提下,也不会考虑是否有等待者,也是直接cas方式去获取锁,成功返回true1、加锁逻辑
ReentrantLock.lock
public void lock() {sync.lock();}
直接调用sync的lock方法,当我们在实例化ReentrantLock时候默认创建的是NoFailLock类,只有实例化ReentrantLock,传递参数true,才会实例化公平锁,这里主要看公平锁的逻辑,更加全面。
FailSync.lock
final void lock() {acquire(1);}
AQS.acquire
public final void acquire(int arg) {//条件一:tryAcquire(arg)尝试获取锁,获取成功返回true 失败返回false//条件二:2.1:addWaiter(Node.EXCLUSIVE), arg) 将当前线程封装node入队列// 2.2:acquireQueued 挂起当前线程,唤醒后相关逻辑// acquireQueued 返回true 表示挂起线程过程中线程被中断唤醒过,false未被中断过if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))//再次设置中断标记为trueselfInterrupt();}
acquire方法主要做了三步:
调用tryAcquire尝试去获取锁,获取锁成功返回true,失败返回false
如果获取锁失败,则通过addWaiter 将当前线程封装为Node节点添加到AQS阻塞队列队尾,并返回当前添加的Node节点
acquireQueued ,将Node作为参数,去执行 将线程park阻塞;线程唤醒之后的逻辑
1、FailSync.tryAcquire
获取当前线程,判断当前锁的状态
- 如果state==0,表示无锁,公平锁讲究先来后到,判断队列是否有等待线程,没有则通过cas方式去更新state的值
当前线程属于重入,直接更新增加state的值
//抢占锁成功返回true,失败返回falseprotected final boolean tryAcquire(int acquires) {//获取当前线程final Thread current = Thread.currentThread();//AQS state值int c = getState();//条件成立,表示当前AQS处于无锁状态if (c == 0) {//条件一:因为FailSync是公平锁,任何时候需要检查一下,队列中是否在当前线程之前有等待者...//hasQueuedPredecessors方法返回true 当前线程前面有等待者,需要入队列//hasQueuedPredecessors方法返回false 当前线程前面没有等待者 ,直接尝试获取锁//条件二:compareAndSetState(0, acquires)//true 说明当前线程抢占锁成功//false 说明存在竞争,抢占锁失败if (!hasQueuedPredecessors() &&compareAndSetState(0, acquires)) {//成功之后设置当前线程为独占锁 线程setExclusiveOwnerThread(current);return true;}}//1. c=state!=0,这种情况需要检查一下当前线程是不是 独占锁的线程,因为ReentranLock是可以重入的//条件成立,说明当前线程就是独占锁的线程else if (current == getExclusiveOwnerThread()) {//重入逻辑//更新state值int nextc = c + acquires;//越界判断if (nextc < 0)throw new Error("Maximum lock count exceeded");//更新值setState(nextc);return true;}//执行到这里?//1. cas失败,c==0,cas state时候发生竞争了,抢占锁失败//2. c>0,且ExclusiveOwnerThread != 当前线程return false;}
2、AQS.addWaiter
当tryAcquire获取锁失败后,会调用addWaiter方法将抢占锁失败的线程,封装为Node节点,addWaiter传参mode是Node.EXCLUSIVE,表示独占状态,意味着重入锁使用了AQS的独占模式来保证线程安全。addWaiter方法分为三步:
当前线程封装为Node节点
判断队列是否为空,不为空则cas tail节点将创建的Node节点添加到队列
如果队列为空或者cas tail节点失败,调用enq 完整入队列
//Node节点入队列,最终返回当前线程封装的Node节点private Node addWaiter(Node mode) {//构建node,把当前线程封装到node中Node node = new Node(Thread.currentThread(), mode);//获取队尾节点Node pred = tail;//快速入队//条件成立,说明当前队列不为空,有node节点了if (pred != null) {node.prev = pred;//cas成功,说明入队成功,false表示发生竞争if (compareAndSetTail(pred, node)) {//前置节点指向创建节点,完成双向绑定pred.next = node;return node;}}//什么情况执行到这里?//1.当前队列是空队列//2. cas tail失败//完整入队enq(node);return node;}
1、AQS.enq
enq就是通过自旋操作将Node节点入队列
这里分为两步:如果队列为空,向队列补充一个Node节点(这种情况发生在第一个获取锁失败的线程,因为占用锁的线程只是将exclusiveOwnerThread设置了下,并没有向阻塞队列里添加节点,因此第一个获取锁失败的线程需要先给占用锁的线程搽屁股)
队列不为空则cas tail方式入队列
//AQS的enq方法private Node enq(final Node node) {//自旋入队,只有入队成功才会跳出循环for (;;) {Node t = tail;//1.当前队列是空队列,tail ==null//说明当前锁被占用,且当前线程 有可能是第一个获取锁失败的线程(当前时刻有可能有多个获取锁失败的线程)if (t == null) { // Must initialize//作为当前持锁线程 第一个 后继线程,需要做什么事?//1.需要创建一个node 将head节点指向该node;因为持有锁的线程当时只是将线程本身赋值给了ExclusiveOwnerThread,并没有向阻塞队列里添加任何node,所以后继节点要为它搽屁股//2.将自己添加到队列if (compareAndSetHead(new Node()))tail = head;//这里并没有直接return} else {//普通入队方式,只不过在for循环中,保证一定入队成功node.prev = t;if (compareAndSetTail(t, node)) {t.next = node;return t;}}}}
3、AQS.acquireQueued
通过addWaiter方法将Node添加到队列后,然后把Node作为参数 调用acquireQueued,去阻塞或者竞争锁
获取当前node的prev前驱节点
如果前驱节点是head节点,head.next节点有权力去尝试获取锁,调用tryAcquire 去尝试获取锁
获取锁成功,将head设置为当前node,将老的head节点出队列
获取锁失败,根据node的waitStatus状态决定是否park Node对应的线程
非公平锁中最后 如果被中断唤醒会通过cancelAcquire 将node出队列
//需要做什么?//1.入队的线程park//2.唤醒线程之后的逻辑//AQS的acquireQueued方法//参数1:node 当前线程包装出来的node,且node节点已经入队成功//2. 当前线程抢占资源成功后,设置state时,使用到的final boolean acquireQueued(final Node node, int arg) {//true表示当前线程抢占锁成功,false表示抢占失败,需要执行出队的逻辑boolean failed = true;try {//当前线程是否被中断boolean interrupted = false;//自旋for (;;) {//什么时候执行这里?//1 在线程未park前//2.线程被unaprk唤醒后//获取当前节点的前置节点final Node p = node.predecessor();//条件1成立;说明当前节点是head.next节点,head.next任何时候有权力尝试再次获取锁//条件2成立:说明head已经释放锁了并且当前node对应的线程(head.next)也获取到锁了//不成立: 说明head对应的线程 还未释放锁,head.next仍需要被parkif (p == head && tryAcquire(arg)) {//设置head为当前node 对应的线程setHead(node);//清除老head节点,协助老的head节点出队p.next = null; // help GC//当前线程获取锁过程中没有异常failed = false;//返回当前线程的中断标记return interrupted;}//shouldParkAfterFailedAcquire() 返回:true 当前线程需要被挂起;false->不需要//parkAndCheckInterrupt()作用?挂起当前线程,并且唤醒之后 返回当前线程的中断标记//唤醒(1.被其他线程unpark方式正常唤醒 2.其他线程给当前挂起线程一个中断信号)if (shouldParkAfterFailedAcquire (p, node) &&parkAndCheckInterrupt())//条件成立:说明当前node对应的线程是被中断唤醒的interrupted = true;}} finally {if (failed)cancelAcquire(node);}}
1、AQS.setHead
private void setHead(Node node) {head = node;node.thread = null;node.prev = null;}
2、AQS.shouldParkAfterFailedAcquire
倘若占用锁的线程还未释放锁,那么其他线程获取锁就会失败,失败就会被封装为Node节点添加到队列,然后调用shouldParkAfterFailedAcquire方法。
该方法主要是 根据获取锁失败Node的前驱节点的 waitStatus状态,判断当前线程在获取锁失败之后 是否要被阻塞挂起,返回true,该线程会被挂起,反之不会被挂起。
Node的waitStatus是什么? Node类有一个waitStatus属性,该属性有5个值,分别为-3(PROPAGATE状态) 、-2(CONDITION状态)-1(SIGNAL状态) 0(默认状态)、1(CANCEL状态)
- CANCEL:线程在队列里被中断唤醒之后,需要将node对应的线程从队列里移除出去,会将node节点的waitStatus状态修改为CANCEL,即取消状态
- 0:node节点默认状态
- SIGNAL:释放锁之后,会通知状态<=0 SIGNAL状态的节点唤醒
- CONDITION:跟condition有关
- PROPAGATE:共享模式下,PROPAGATE状态的线程处于可运行状态
waitStatus ==SIGNAL 状态,返回true
waitStatus >0 (CANCEL状态),通过循环将 CANCEL状态的节点移除出去
waitStatus ==0 cas方式将node节点值修改为 SIGNAL状态
//AQS 的shouldParkAfterFailedAcquire方法//总结//1.当前节点的前置节点的状态是 取消cancel状态, 第一次来到该方法,会越过取消状态的节点返回false,第二次来 会返回true然后park当前线程//2.当前节点的前置节点状态是 0状态,强制设置前置节点是 singal(-1),第二次自旋来到这个方法时返回true,然后park当前线程//参数一:pred 当前线程的node 的前置节点//参数二:node 当前线程对应的node节点//返回值 true表示当前线程需要挂起;false->不需要private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {//获取前置节点的状态//0 默认状态new node();-1 singal 表示当前节点释放锁之后会唤醒他的第一个后继节点;1 cancel状态int ws = pred.waitStatus;//条件成立:表示前置节点是一个可以唤醒当前节点的节点,所有返回true->parkAndCheckInterrupt()方法park当前线程//普通情况,第一次来到该方法,ws不会是-1if (ws == Node.SIGNAL)return true;//条件成立,ws前置节点是cancel状态if (ws > 0) {//从node节点开始向前面找ws<=0的节点,do {node.prev = pred = pred.prev;} while (pred.waitStatus > 0);//找到之后,退出循环,过程中会将ws>0(cancel状态的节点出队)pred.next = node;} else {//当前node前置节点的ws 状态就是 0情况下//将当前线程node的前置节点 状态强制设置为-1 singal,表示前置节点释放锁之后 需要唤醒我...compareAndSetWaitStatus(pred, ws, Node.SIGNAL);}return false;}
返回true,获取锁失败的线程会调用parkAndCheckInterrupt ,让线程挂起;返回false,不需要挂起。
3、AQS.parkAndCheckInterrupt
shouldParkAfterFailedAcquire返回true之后,调用parkAndCheckInterrupt 让线程挂起
private final boolean parkAndCheckInterrupt() {LockSupport.park(this);return Thread.interrupted();//返回线程是否被中断挂起的布尔值}
4、AQS.cancelAcquire
当线程被中断唤醒之后,parkAndCheckInterrupt方法返回true,不带中断的获取锁方法不会做任何操作,带中断的获取锁方法会直接抛出中断异常,然后通过将cancelAcquire将被中断唤醒的node节点出队列。出队列根据当前node的位置分为三种情况:
node节点是tail队尾节点,cas方式直接出队列
- node节点是 head.next节点
- node节点既不是head.next节点也不是tail节点 ```java
private void cancelAcquire(Node node) { if (node == null) return; //因为已经取消排队了,直接将node关联的线程thread置为null node.thread = null; //获取当前取消排队node的前继节点 Node pred = node.prev; while (pred.waitStatus > 0) node.prev = pred = pred.prev; //拿到前驱的后继节点 //1.当前node节点 //2.waitStatus>0的节点 Node predNext = pred.next; //将当前node的状态设置为 取消状态 1 node.waitStatus = Node.CANCELLED;
//当前node在队列的位置不同,执行的出队策略也不同,分三种情况://1.当前node节点是tail 队尾节点//2.当前node节点不是head.next节点也不是tail节点//3.当前node节点是head.next节点//条件一 node == tail 成立:当前node节点是tail节点//条件二 compareAndSetTail(node, pred) 成立,说明修改tail成功//情况1 当前node节点是tail节点if (node == tail && compareAndSetTail(node, pred)) {//修改pred.next置为null,完成node出队compareAndSetNext(pred, predNext, null);} else {//保存node的状态int ws;//第二种情况,当前node 不是head.next也不是tail节点//条件一:pred != head 成立 当前node 不是head.next也不是tail节点//条件二:((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) 是为了保证前驱节点的状态是singal状态,因为if里面做的事情就是pred.next -> node.nextif (pred != head &&((ws = pred.waitStatus) == Node.SIGNAL ||(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&pred.thread != null) {//情况2 当前node 不是head.next也不是tail节点//出队 pred.next -> node.next节点后,当node.next节点被唤醒后,调用shouldParkAfterFailedAcquire//会让node.next节点越过取消状态的节点,完成真正出队Node next = node.next;if (next != null && next.waitStatus <= 0)compareAndSetNext(pred, predNext, next);} else {//情况3 当前node节点是head.next节点//类似情况2 node的后继节点唤醒后,调用shouldParkAfterFailedAcquire会让node.next跳过取消状态的node节点,也就会跳过要出队的node节点,让node.next与head进行绑定unparkSuccessor(node);}node.next = node; // help GC}
}
<a name="TDSTu"></a>### 2、释放锁逻辑调用ReentrantLock.unlock方法,释放锁,会发生什么?<a name="JslSG"></a>#### 1、reentrantLock.unlock```javapublic void unlock() {sync.release(1);}
2、AQS.release
release方法尝试去释放锁,如果锁完全释放成功,通过unparkSuccessor方法唤醒队列里的一个Node节点
public final boolean release(int arg) {//tryRelease 返回true 当前线程完全释放锁;false当前线程尚未完全释放锁if (tryRelease(arg)) {//head节点是在有一个线程占用锁期间,有其他线程发现获取不了且队列是空时候,后续线程会创建一个节点赋值给head//拿到head节点Node h = head;//条件1成立 说明队列的head已经初始化了,ReentranLock在使用期间发生了 多线程竞争//条件二成立 说明当前head后面有节点if (h != null && h.waitStatus != 0)//去唤醒后继节点unparkSuccessor(h);return true;}return false;}
1、Sync.tryRelease
方法去尝试释放锁,因为reentrantLock是允许重入锁的,因此返回true表示占用锁线程完全释放锁,false表示未完全释放锁
//true 表示当前线程完全释放锁;false未完全释放锁protected final boolean tryRelease(int releases) {//减去释放的值int c = getState() - releases;//在释放锁之前 先判断下当前线程是否是占用锁的线程,不是抛出异常if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();//是否已经完全释放锁boolean free = false;//条件成立,说明当前线程已经达到完全释放锁的条件if (c == 0) {free = true;//设置ExclusiveOwnerThread为nullsetExclusiveOwnerThread(null);}//更新AQS.statesetState(c);return free;}
2、AQS.unparkSuccessor
unparkSuccessor方法做了两步操作
- 将当前head节点的waitStatus cas方式 从signal转为 0
获取head.next节点,从tail节点开始从后向前,查找最靠近head的 状态小于等于0的Node节点,如果存在,则唤醒该节点
//AQS的unparkSuccessor方法//唤醒当前节点的下一个节点private void unparkSuccessor(Node node) {int ws = node.waitStatus;//singal状态if (ws < 0)//当前节点已经完成唤醒后继结点的任务了compareAndSetWaitStatus(node, ws, 0);Node s = node.next;//s什么时候为null//1.node==tail节点//2 新节点入队未完成时(1.node.prev=pred 2.cas tail 3. pred.next=node 第三步未完成)//条件2:前置条件s!=null, 成立说明,当前node的后置节点是cancel状态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;//上面循环,从队尾开始向前查找,会找到一个离当前node最近的一个可以被唤醒的node(节点状态<=0),node 可能找不到 有可能是null}//如果找到合适的可以被唤醒的node,unpark唤醒if (s != null)LockSupport.unpark(s.thread);}
当唤醒节点之后,因为我们是在acquireQueued方法里面自旋被挂起的,因此唤醒之后回到自旋,判断当前线程是不是head.next节点,如果是,尝试获取锁,获取锁成功则将head指向现在占用锁的线程,将老的head节点出队列
以上是对公平锁的分析,那么非公平锁又有什么不同呢?
- 非公平锁在获取锁的时候,会先通过cas 去获取锁,公平锁不会
- 在cas失败后,调用同样的acquire方法,去获取锁,这里不同的是,当无锁情况下,非公平锁不会判断队列是否有等待者,而是直接cas去获取锁;公平锁会判断队列是否有等待者,没有的话才cas去获取锁。
总结:
ReenTranLock lock 方法先尝试获取锁,获取不到则将线程封装为node节点,保存到阻塞队列,然后在自旋中 完成park阻塞当前线程和被唤醒后,看是否是head.next节点,是的话尝试获取锁,获取成功则替换老的head节点。
带有中断机制的lockInterruptibly方法,在阻塞线程中如果线程被中断信号唤醒,会抛异常,调用cancelAcquire方法让node出队列,修改node的状态为cancel状态
出队列分三种情况:
- node是tail队尾节点 直接让node出队列
- 当前node节点是head.next节点 唤醒node的后继节点,在后继节点唤醒之后执行出node操作
当前node节点不是head.next节点也不是tail节点 让node的前继节点指向node的后继节点
五、condition条件队列
1. AQS.ConditionObject
public class ConditionObject implements Condition, java.io.Serializable {private static final long serialVersionUID = 1173984872572414699L;//指向条件队列的第一个node节点private transient Node firstWaiter;//指向条件队列的最后一个node节点private transient Node lastWaiter;}
2. Condition接口
public interface Condition {void await() throws InterruptedException;void awaitUninterruptibly();long awaitNanos(long nanosTimeout) throws InterruptedException;boolean await(long time, TimeUnit unit) throws InterruptedException;boolean awaitUntil(Date deadline) throws InterruptedException;void signal();void signalAll();}
3. lock.newCondition
创建一个等待队列
public Condition newCondition() {return sync.newCondition();}
final ConditionObject newCondition() {return new ConditionObject();}
4.Condition.await
这个方法的逻辑就是,先判断当前线程是否被中断,是则直接抛中断异常,否则
第一步:将当前线程封装为node节点,添加到条件队列队尾
- 第二步:释放当前线程所占用的所有锁,(只有释放了自己占用的锁,才能让其他线程唤醒自己)
第三步:循环判断是否有占用锁的线程调用了 当前线程的signal方法
- 如果调用了signal方法,表示当前线程已经迁移到了阻塞队列,接下来参与竞争锁的逻辑
如果没有调用signal方法,表示当前线程还在条件队列里,此时走 挂起该线程的逻辑;其中如果在条件队列里挂起过程中,被其他线程中断信号唤醒,也会退出循环,参与竞争锁的逻辑
public final void await() throws InterruptedException {//判断当前线程是否已经是中断状态,如果是直接抛出中断异常if (Thread.interrupted())throw new InterruptedException();//将调用await方法的线程包装成node并加入到条件队列,并返回当前线程的nodeNode node = addConditionWaiter();//完全释放掉当前线程对应的锁(将state置为0)//为什么要释放锁呢? 自己加着锁,然后挂起后,谁还能来唤醒你呢int savedState = fullyRelease(node);//0 在condition队列挂起期间未接收过中断信号//-1 在condition队列挂起期间接收到中断信号了//1 在condition队列挂起期间未接收到中断信号,但是在迁移到 AQS的阻塞队列 之后,接收过中断信号int interruptMode = 0;//isOnSyncQueue方法返回true 表示当前线程已经迁移到 AQS的阻塞队列了//返回false 说明当前node还在条件队列中,需要继续parkwhile (!isOnSyncQueue(node)) {//挂起当前node对应的线程LockSupport.park(this);//什么时候会被唤醒?//1.正常情况,当前线程被其他线程调用signal方法转移到阻塞队列,然后获取到锁之后,会唤醒//2.迁移到阻塞队列中,发现阻塞队列中的前驱节点状态 是取消状态或者修改前驱节点状态-1失败,会唤醒当前节点//3.当前节点在条件队列被park挂起期间,被外部线程中断信号唤醒//checkInterruptWhileWaiting:就算再condition条件队列挂起期间 线程发生中断了,对应的node也会被迁移到 AQS的阻塞队列if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)break;}//执行到这里,说明当前node被其他线程调用signal方法,已经迁移到 AQS的阻塞队列了//acquireQueued(node, savedState) 条件成立,表示在阻塞队列中被中断唤醒过//interruptMode != THROW_IE 成立 说明当前node在条件队列内未发生过中断唤醒//条件成立:说明当前线程在阻塞队列竞争锁的时候发生中断, && 中断标识位不是 THROW_IEif (acquireQueued(node, savedState) && interruptMode != THROW_IE)//将当前线程的中断标识位设置为重新中断interruptMode = REINTERRUPT;//条件成立,代表当前node在条件队列中被中断唤醒了,会加入到阻塞队列,但是并未设置node.nextWaiter==nullif (node.nextWaiter != null)//清理条件队列内取消状态节点,让取消状态节点出队列unlinkCancelledWaiters();//条件成立,说明挂起期间被中断过 1条件队列被中断,2条件队列之外被中断if (interruptMode != 0)reportInterruptAfterWait(interruptMode);}
5. condition.addConditionWaiter
线程入条件队列逻辑
判断队列是否有元素,有但是元素状态不对,则进行一次全队的清理工作,清除取消状态的节点,重新获取队尾节点,因为有可能清除时候改变了队尾节点
- 线程封装为node节点,判断队列是否有元素?
- 如果没有,将头节点指向node
- 如果有,将队尾节点的next指向node
- 更新队尾节点为node,并返回当前线程的node
//将当前线程封装为node添加到condition条件队列去//调用await的线程 都是已经持有锁的状态了,因此这里不会发生并发private Node addConditionWaiter() {//获取当前队列的尾节点 保存到局部变量t中Node t = lastWaiter;//条件一:t != null条件成立:说明队列已经有元素了//node在条件队列里面 状态是-2 CONDITION//条件二:t.waitStatus != Node.CONDITION 成立,说明当前node发生中断了...if (t != null && t.waitStatus != Node.CONDITION) {//清理条件队列中所有 取消状态的节点unlinkCancelledWaiters();//更新局部变量t的引用为最新的队尾节点,因为unlinkCancelledWaiters方法可能会修改尾节点lastWaiter引用t = lastWaiter;}//为当前线程创建node节点,设置状态为condition状态(-2)Node node = new Node(Thread.currentThread(), Node.CONDITION);//条件成立:说明条件队列没有任何元素,当前线程是第一个入队元素,让firstWaiter指向当前nodeif (t == null)firstWaiter = node;//当前条件队列有元素了,做追加操作elset.nextWaiter = node;//更新队尾节点为当前nodelastWaiter = node;//返回当前线程的nodereturn node;}
6.AQS.fullyRelease
释放当前线程node占用的所有锁资源逻辑
- 正常情况会全部释放掉锁资源,有一种情况,那就是未占用锁的线程调用了await方法,这是错误写法,出现了的话,会将当前线程对应的node节点修改为取消状态,那什么时候会将取消状态清除出去?后面节点入队列时候发现队尾节点状态不对,会将取消状态节点清除出去。
- 如果成功释放了占用的锁,为什么要返回当前线程释放的state值?
- 因为当线程node被迁移到AQS阻塞队列时,再次被唤醒,且当前node是head.next节点,而且当前锁状态state==0时,当前node可以获取到锁,此时需要将 state 的值设置为savedState ```java
final int fullyRelease(Node node) { //完全释放锁是否成功:当failed失败时,说明当前线程是在未先调用lock方法前提下,直接调用await方法的线程(这是错误的写法) //假设失败,在finally代码块会将刚刚入条件队列的node节点的状态设置为取消状态,后继节点线程 就会将取消状态的节点 给清理出去 boolean failed = true; try { //获取当前线程 所持有的state值 int savedState = getState(); //绝大部分情况下,这里会返回true if (release(savedState)) { //失败标记设置为false failed = false; //返回当前线程释放的state值 //这里为什么要返回savedState? //因为当线程node被迁移到AQS阻塞队列时,再次被唤醒,且当前node是head.next节点,而且当前锁状态state==0时,当前ndoe可以获取到锁, //此时需要将 state 的值设置为savedState return savedState; } else { throw new IllegalMonitorStateException(); } } finally { if (failed) node.waitStatus = Node.CANCELLED; } }
<a name="Kig77"></a>### 7. AQS.isOnSyncQueue判断当前node是否在AQS的阻塞队列,返回true代表已经在阻塞队列,false则不在阻塞队列- 第一个if判断- **节点状态==condition(-2)说明节点一定在条件队列里**- 当前节点的前驱节点为空,首先节点的状态一定不为-2,可能是 0 或者 1,1表示未占用锁的线程调用了await方法,让node节点状态变成 1 取消状态;0说明当前线程已经被signal方法唤醒,因为唤醒操作是先修改节点状态,再迁移到阻塞队列,所以前驱节点为空,说明此时只是节点状态改变了,但还没有入阻塞队列- 第二个if判断- 执行到第二个if判断,可以排除掉一种情况,那就是node节点状态==1取消状态这种情况,因为signal方法是不会把取消状态的节点迁移到阻塞队列里去- **条件成立?为啥一定在阻塞队列里?**- 因为入阻塞队列是在enq方法,入队逻辑有三步:1.设置node.prev= tail;2.cas当前node为阻塞队列的节点;3.设置前驱的后继节点指向node =》pred.next==当前node- 所以就算node的前驱不为空,也不能判断node已经在阻塞队列里了,也有可能是正在过程中,而node的后继节点不为空,则表示后面有元素,一定在队列里了,返回true- 执行到findNodeFromTail 这里,说明当前node可能已经在队列里了,只是后面没有后继节点,因此调用findNodeFromTail方法从后向前查找,**找到了返回true,找不到说明不在阻塞队列里**```java//判断当前node是否在AQS的阻塞队列final boolean isOnSyncQueue(Node node) {//条件一node.waitStatus == Node.CONDITION成立//说明当前node一定在条件队列,因为singal方法迁移node节点到 阻塞队列时,会将node节点状态设置为0//前置条件:状态不为condition//node.waitStatus == 0 (表示当前节点已经被signal了)//node.waitStatus == 1 (当前线程是未持有锁调用的await方法...进入到该方法前状态被修改为取消状态)//node.waitStatus == 0 为什么还要判断node.prev ==null?//因为signal方法 是先修改状态,再迁移if (node.waitStatus == Node.CONDITION || node.prev == null)return false;//执行到这里,node.waitStatus!=condition&&node.prev!=null可以排除 node.waitStatus==1 取消状态//为什么可以排除 取消状态? 因为signal方法是不会把 取消状态的node节点迁移到 阻塞队列里的//设置prev引用的逻辑是 迁移 阻塞队列设置的enq()//入队逻辑:1.设置node.prev =tail;2. cas当前node为阻塞队列尾节点,cas成功才算真正入阻塞队列;3.pred.next=node//可以推算出,就算prev不为nul,也不能说明node已经成功入阻塞队列了//条件成立,说明当前节点后面有元素了,那么已经迁移到阻塞队列了if (node.next != null)return true;//执行到这里,说明当前节点的状态是node.prev!=null &&node.waitStatus == 0//findNodeFromTail:从AQS的阻塞队列尾开始向前查找node,查找到返回true,找不到返回falsereturn findNodeFromTail(node);}
8. Condition.signal
- 先判断当前线程是不是占用锁的线程,不是直接抛出异常
获取条件队列的头节点,如果不为空,表示队列有元素,调用doSignal唤醒条件队列的第一个节点
public final void signal() {//判断signal方法的线程是否是独占锁的线程,如果不是,那么抛出异常if (!isHeldExclusively())throw new IllegalMonitorStateException();//获取条件队列的第一个节点Node first = firstWaiter;//如果首节点不为空if (first != null)//唤醒条件队列的首节点线程doSignal(first);}
9. Condition.doSignal
唤醒条件队列的首个节点逻辑
如果头节点的下一个节点为空,说明头节点出队列之后,头尾节点都为空,也就是条件队列没有元素了
- 断开当前头节点的next
- 调用transferForSignal 将当前头节点迁移到阻塞队列里,返回true代表迁移成功,false说明迁移失败
- 循环的条件是:如果头节点迁移失败,那么会将头节点设置为头节点的下一个节点继续尝试,直至迁移一个节点成功,或者头节点为空
什么时候头节点迁移失败?
- node节点状态是取消状态(非占用锁的线程调用await方法)
node对应的线程在挂起期间被其他线程使用中断信号唤醒,这时候也会主动迁移到阻塞队列里,这时节点状态也会修改为0
private void doSignal(Node first) {do {//firstWaiter = first.nextWaiter 因为first马上要出队列了,让firstWaiter指向下一个节点//如果当前节点的下一个节点为null,说明只有这一个节点,当前节点出队后,需要修改lastWaiter为nullif ( (firstWaiter = first.nextWaiter) == null)lastWaiter = null;//当前first节点 出条件队列,断开与下一个节点的关系first.nextWaiter = null;//transferForSignal true 表示当前first迁移到阻塞队列成功,false说明迁移失败,//while循环:(first = firstWaiter) != null 当前first节点迁移失败,将当前first更新为first.next继续尝试迁移,会继续向后尝试迁移节点,直到迁移一个节点成功,或者条件队列为null为止} while (!transferForSignal (first) &&(first = firstWaiter) != null);}
10. AQS.transferForSignal
迁移node节点到阻塞队列逻辑,迁移成功返回true,迁移失败返回false
通常返回true,什么时候返回false?
- node节点状态是取消状态(非占用锁的线程调用await方法)
- node对应的线程在挂起期间被其他线程使用中断信号唤醒,这时候也会主动迁移到阻塞队列里,这时节点状态也会修改为0
- 第一步:先cas方式将node状态从-2 修改为0 ,设置成功后才继续往下,那什么时候会失败呢?
- node状态是cancel取消状态(非占用锁的线程调用await方法)或者node对应的线程在挂起期间被其他线程使用中断信号唤醒,这时候也会主动迁移到阻塞队列里,这时节点状态也会修改为0
- cas成功后第二步:enq方法将node入阻塞队列,返回node的前驱节点
- 第三步:
- 条件一:前驱节点的状态> 0,说明前驱节点状态为 取消状态
- 条件二:前置条件:前驱节点状态<=0,cas修改前驱节点状态为 -1 signal状态失败
- 条件一或者条件二满足一个 都将唤醒刚入队的node对应的线程
执行完dosignal方法后,node就从条件队列迁移到阻塞队列里了,会判断其前驱节点的 waitStatus,如果大于0(是取消状态)或者设置状态为-1 (signal状态),这个时候会直接唤醒node,否则就按AQS的正常情况,竞争锁的逻辑final boolean transferForSignal(Node node) {//cas修改当前node节点状态为0,因为当前节点 之后要迁移到AQS的阻塞队列了//成功:当前节点在条件队列中状态正常//失败:1.取消状态(线程await方法时候,未持有锁,最终线程对应的node会被设置为cancel 取消状态)// 2.node对应的线程 挂起期间,被其他线程使用 中断信号唤醒过...(就会主动进入到阻塞队列,这时也会修改状态为0)if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))return false;//将node入AQS的阻塞队列,返回node的前驱节点Node p = enq(node);//获取前驱节点的状态int ws = p.waitStatus;//条件一:成立 说明前驱节点的状态在阻塞队列里是取消状态,唤醒当前节点//条件二:前置条件:ws<=0//compareAndSetWaitStatus(p, ws, Node.SIGNAL) 返回true 表示设置前驱节点为signal状态成功//compareAndSetWaitStatus(p, ws, Node.SIGNAL) 返回false =>什么时候返回false?//当前驱node对应的线程是 lockInterrupt入队的node时候,是会响应中断的,外部线程给前驱线程中断信号后,前驱节点会将状态修改为取消状态,并执行出队逻辑//前驱node状态只要不是 0 或者 -1 就会唤醒当前线程if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))//唤醒当前node对应的线程LockSupport.unpark(node.thread);return true;}
11. Condition.await
在上面分析await方法时候,线程会被添加到条件队列,释放锁,在循环中被阻塞。而通过其他线程调用signal方法,将线程从条件队列唤醒之后,退出循环,继续后面逻辑。
在循环里有 一个方法,checkInterruptWhileWaiting 这个方法是做什么的呢?顾名思义:判断线程在条件队列阻塞期间,有没有被其他线程使用中断信号唤醒过
如果线程阻塞期间被中断唤醒,也会将节点迁移到阻塞队列中,参与AQS竞争,对应 if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) 条件成立
- 在参与竞争期间仍被中断唤醒,判断中断模式是不是 THROW_IE,不是则代表该node在阻塞期间被中断唤醒过,只需要重新中断响应即可
- 如果node的后继节点不为空,说明当前node是在条件队列里被中断唤醒,这种情况下虽然node被迁移到了阻塞队列里,但是并没有断开与后继节点的联系,会进行一次全队的清理工作,清除取消状态节点
最后判断中断模式,如果是在条件队列被中断唤醒 抛出异常,在阻塞队列被中断唤醒
public final void await() throws InterruptedException {//判断当前线程是否已经是中断状态,如果是直接抛出中断异常if (Thread.interrupted())throw new InterruptedException();//将调用await方法的线程包装成node并加入到条件队列,并返回当前线程的nodeNode node = addConditionWaiter();//完全释放掉当前线程对应的锁(将state置为0)//为什么要释放锁呢? 自己加着锁,然后挂起后,谁还能来唤醒你呢int savedState = fullyRelease(node);//0 在condition队列挂起期间未接收过中断信号//-1 在condition队列挂起期间接收到中断信号了//1 在condition队列挂起期间未接收到中断信号,但是在迁移到 AQS的阻塞队列 之后,接收过中断信号int interruptMode = 0;//isOnSyncQueue方法返回true 表示当前线程已经迁移到 AQS的阻塞队列了//返回false 说明当前node还在条件队列中,需要继续parkwhile (!isOnSyncQueue(node)) {//挂起当前node对应的线程LockSupport.park(this);//什么时候会被唤醒?//1.正常情况,当前线程被其他线程调用singal方法转移到阻塞队列,然后获取到锁之后,会唤醒//2.迁移到阻塞队列中,发现阻塞队列中的前驱节点状态 是取消状态或者修改前驱节点状态-1失败,会唤醒当前节点//3.当前节点在等待队列被park期间,被外部线程中断唤醒//checkInterruptWhileWaiting:就算在condition条件队列挂起期间 线程发生中断了,对应的node也会被迁移到 AQS的阻塞队列if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)break;}//执行到这里,说明当前node已经迁移到 AQS的阻塞队列了//acquireQueued(node, savedState) 条件成立,表示在阻塞队列中被中断唤醒过//interruptMode != THROW_IE 成立 说明当前node在条件队列内未发生过中断唤醒//条件成立:说明当前线程在阻塞队列竞争锁的时候发生中断, && 中断标识位不是 THROW_IEif (acquireQueued(node, savedState) && interruptMode != THROW_IE)//将当前线程的中断标识位设置为重新中断interruptMode = REINTERRUPT;//条件成立,代表当前node在条件队列中被中断唤醒了,会加入到阻塞队列,但是并未设置node.nextWaiter==nullif (node.nextWaiter != null)//清理条件队列内取消状态节点,让取消状态节点出队列unlinkCancelledWaiters();//条件成立,说明挂起期间被中断过 1条件队列被中断,2条件队列之外被中断if (interruptMode != 0)reportInterruptAfterWait(interruptMode);}
12. Condition.checkInterruptWhileWaiting
如果被中断过了,需要调用 transferAfterCancelledWait方法去判断 是在条件队列里被中断唤醒还是在阻塞队列里被中断唤醒的,根据这个然后觉得 抛出中断异常 还是 重新中断
- 返回 THROW_IE 表示在条件队列里阻塞过程中被其他线程中断唤醒
返回 REINTERRUPT 表示在阻塞队列里被中断唤醒,也就是其他线程先调用signal方法唤醒node,入阻塞队列之后在等待竞争锁的时候,node被其他线程中断唤醒了
private int checkInterruptWhileWaiting(Node node) {//Thread.interrupted()返回当前线程中断标记位,并且重置当前标记为falsereturn Thread.interrupted() ?//如果是被中断唤醒了,需要判断是在条件队列里面被中断唤醒 还是在阻塞队列里面被中断唤醒(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :0;}
13. AQS.transferAfterCancelledWait
返回true表示当前节点是在条件队列里面被中断唤醒,false表示当前节点不是在条件队列里被中断唤醒
如果cas节点状态成功,会把节点迁移到阻塞队列里,但并没有将node的后继节点断开,正常通过signal方法唤醒的线程,会迁移到阻塞队列里并断开与后继节点的联系。//返回true表示当前节点是在条件队列里面被中断唤醒,false表示当前节点不是在条件队列里被中断唤醒final boolean transferAfterCancelledWait(Node node) {//条件成立,代表cas修改节点状态成功,说明当前node一定在条件队列内,因为调用signal方法迁移节点到阻塞队列时,会将node节点从-2修改为0//这里有个知识点,阻塞的线程被唤醒,不一定是通过lockSupport.unpark方式唤醒,也可能通过线程.interrupt()方法更新了中断标识,并唤醒了被阻塞的线程if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {//在条件队列中阻塞被中断唤醒的node,也会被迁移到阻塞队列里面enq(node);//返回true表示 在条件队列里面被中断唤醒的return true;}//如果cas失败,则判断node是否已经在阻塞队列里//执行到这里 几种情况?//1.当前node已经被外部线程调用signal方法 迁移到阻塞队列了//2.当前node正在被外部线程调用signal方法 迁移到阻塞队列中while (!isOnSyncQueue(node))Thread.yield();//返回false 表示当前节点 被中断唤醒时候,不在条件队列了return false;}
14. AQS.reportInterruptAfterWait
根据中断模式进行处理,是 THROW_IE 直接抛出中断异常,否则设置当前线程中断标记位为true,如果程序响应中断,走响应中断程序;不响应中断,什么都不操作
private void reportInterruptAfterWait(int interruptMode)throws InterruptedException {//条件成立:说明是在条件队列中被中断,直接抛出异常if (interruptMode == THROW_IE)throw new InterruptedException();//条件成立:说明是在条件队列之外被中断,设置当前线程中断标记位为true,如果程序是不响应中断的,那么程序是不会响应中断的else if (interruptMode == REINTERRUPT)selfInterrupt();}
总结
线程A先通过lock.lock方法获取锁成功,然后调用await方法 将线程封装为node添加到条件队列,释放线程A所有锁,进入循环判断node是否在AQS阻塞队列,没有则阻塞线程A;另外一个线程B 通过lock.lock方法获取到锁之后,调用了signal或siganlAll方法,将条件队列的节点迁移到阻塞队列,使得线程A有机会进入到阻塞队列,从而退出循环,然后参与AQS的竞争锁逻辑。如果线程A在最开始lock.lock方法获取锁失败则会直接添加到AQS的阻塞队列,参与AQS的竞争锁逻辑。
- await:阻塞-》将线程封装为node节点,释放锁资源,循环里阻塞线程,被其他线程调用signal、siganlAll方法,迁移到阻塞队列后,退出循环,参与AQS竞争锁逻辑
- signal:释放-》从条件队列的头节点开始向后,先cas修改node节点状态为 0 然后迁移到AQS阻塞队列返回node的前驱节点,如果前驱节点是取消状态 或者cas 状态为 -1 signal失败,则LockSupport.unpark方式唤醒该node节点
