Lock底层原理—ReentrantLock、AQS、Condition
Lock底层原理—ReentrantLock、AQS、Condition
[TOC]
先来看看J.U.C包下的结构
- juc-locks 锁框架
- juc-atomic 原子类框架
- juc-sync 同步器框架
- juc-collections 集合框架
- juc-executors 执行器框架
而我们今天的主角ReentrantLock
,就是juc-locks包下的。上一篇刚算了解一下synchronized
的底层原理,所以就想看看ReentrantLock
和它的区别到底是什么,改进在哪里,适用于什么场景。
1. Lock
Lock
和ReadWriteLock
是两大锁的根接口,下面看一下JDK 1.8 API中如何描述的
通过API的介绍,sychronized
和Lock
的区别可以分为如下:
Lock
增加了灵活性,最主要的就是支持多个Condition。Lock
提供了非阻塞获得锁的方式,synchronized
有自旋锁。Lock
更适合使用在复杂的同步,而synchronized
更简单、简杰。Lock
可以设置公平锁和非公平锁,synchronized
只有非公平锁。
浏览一下Lock
源码
package java.util.concurrent.locks; import java.util.concurrent.TimeUnit; public interface Lock { //获得锁 void lock(); //获取锁定,除非当前线程中断 void lockInterruptibly() throws InterruptedException; //只有在调用时才可以获得锁 boolean tryLock(); //如果在给定的等待时间内是空闲的,并且当前的线程尚未得到 interrupted,则获取该锁 boolean tryLock(long time, TimeUnit unit) throws InterruptedException; //释放锁。 void unlock(); //返回一个新Condition绑定到该实例Lock实例 Condition newCondition(); }
2. ReentrantLock
Lock
了解之后,看一下实现类是如何实现接口的方法的。最常用的实现类就是ReentrantLock
。
2.1. 公平锁和非公平锁
public ReentrantLock() { sync = new NonfairSync(); } public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
从构造函数上可以看出来ReentrantLock
实现了公平锁和非公平锁两种,默认一般使用非公平锁,它的效率和吞吐量都比公平锁高的多。
2.2. Sync
abstract static class Sync extends AbstractQueuedSynchronizer { private static final long serialVersionUID = -5179523762034025860L; abstract void lock(); 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; } 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; setExclusiveOwnerThread(null); } setState(c); return free; } protected final boolean isHeldExclusively() { return getExclusiveOwnerThread() == Thread.currentThread(); } final ConditionObject newCondition() { return new ConditionObject(); } final Thread getOwner() { return getState() == 0 ? null : getExclusiveOwnerThread(); } final int getHoldCount() { return isHeldExclusively() ? getState() : 0; } final boolean isLocked() { return getState() != 0; } private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { s.defaultReadObject(); setState(0); // reset to unlocked state } }
2.3. NonfairSync
非公平锁实现
static final class NonfairSync extends Sync { private static final long serialVersionUID = 7316153563782823691L; final void lock() { /** * 先抢锁 */ if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); } protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); } }
2.4. FairSync
说完了非公平锁,那就不得不提公平锁的实现了。
static final class FairSync extends Sync { private static final long serialVersionUID = -3000897897090466540L; final void lock() { acquire(1); } //非公平锁调用的是父类sync的nonfairTryAcquire方法 protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; } }
有了非公平锁做铺垫,了解公平锁就方便多了。公平锁和非公平锁不同之处在于,公平锁在获取锁的时候,不会先去检查state状态。
公平锁和非公平锁最主要的区别在于lock()
,一个是先排队,一个是先抢锁。但是非公平锁的吞吐量更大。
通过查看ReentrantLock
中的底层实现,我们发现了AQS的踪迹,先看一下AQS是什么,然后分析一下整个锁的流程。
3. AbtractQueuedSynchronizer
AbtractQueuedSynchronizer
抽象的队列式的同步器,AQS定义了一套多线程访问共享资源的同步器框架,是JUC包下整体的基础框架。AQS是一个抽象的队列同步器,维护着一个同步队列和State状态 。底层使用了Unsafe类进行CAS操作。Sync
就是AbstractQueuedSynchronizer
的子类,AQS采用了模板方法模式,提高了扩展性有保证了规范性。
AQS只是一个框架,具体资源的获取/释放方式交由自定义同步器去实现
JDK 1.8 API中的介绍如下
从上述介绍中可以看出:
- 同步器依赖于每个Node的状态,状态都是通过CAS来设置的。
- AQS支持独占模式和共享模式 。
还有一些关于
Condition
,下面再去研究。
看一下其内部图片:
AbtractQueuedSynchronizer
内部维持着一个虚拟的双向同步队列,因为队列中只有头节点和尾节点的指针。
Node
是AQS中的节点类
/** Marker to indicate a node is waiting in shared mode */ static final Node SHARED = new Node(); /** Marker to indicate a node is waiting in exclusive mode */ static final Node EXCLUSIVE = null; /** waitStatus value to indicate thread has cancelled */ static final int CANCELLED = 1; /** waitStatus value to indicate successor's thread needs unparking */ static final int SIGNAL = -1; /** waitStatus value to indicate thread is waiting on condition */ static final int CONDITION = -2; /** * waitStatus value to indicate the next acquireShared should * unconditionally propagate */ static final int PROPAGATE = -3; volatile int waitStatus; volatile Node prev; volatile Node next; volatile Thread thread; Node nextWaiter;
waitstatus
查看状态值
- CANCELLED:当线程等待超时或者被中断,则取消等待,设等待状态为-1,进入取消状态则不再变化
- SIGNAL:后继节点处于等待状态,当前节点被取消或中断时会通知后继节点,使后继节点的线程得以运行
- CONDITION:当前节点处于等待队列,节点线程等待在Condition上,当其他线程对condition执行signall方法时,等待队列转移到同步队列,加入到对同步状态的获取
- PROPAGATE:与共享模式相关,在共享模式中,该状态标识结点的线程处于可运行状态
- 0状态:值为0,代表初始化状态。
那么SHARED
和EXCLUSIVE
是干什么的呢?
SHARED
标识的是共享,而EXCLUSIVE
是独占。但是ReentrantLock
是排他锁。
看一下最主要的lock方法
通过CAS判断当前状态是否为空闲状态(0),如果是则将状态设置为独占线程(1),这样获取锁成功。CAS操作只保证一个线程获取锁,如果多个线程竞争,失败的就需要排队。
所以else就是失败者走的,古有名言:失败是成功之母。那就看一下失败者的经历吧!!!
acquire
是父类AbstractQueuedSynchronizer
的方法。
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
tryAcquire
方法Nonfair
重写了
protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); }
final boolean nonfairTryAcquire(int acquires) { //我们走下来 acquires = 1 final Thread current = Thread.currentThread(); //获取锁状态 int c = getState(); //如果是0,表示没有线程拥有 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"); //更新state值 setState(nextc); return true; } //尝试获取失败 return false; }
第一个吃螃蟹的当不上,只能当下一个了。再次尝试获取,如果此时状态为空闲,则获取;如果有线程占用,判断是不是自己;如果是,更新状态值;如果不是,则失败。
!tryAcquire(arg)
-> acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
慢慢往下撸
private Node addWaiter(Node mode) { //包装成一个结点 Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure Node pred = tail; if (pred != null) { node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } //无头节点,进入enq创建队列也就是头节点 enq(node); return node; } private Node enq(final Node node) { for (;;) { Node t = tail; if (t == null) { //CAS操作设置头节点 if (compareAndSetHead(new Node())) tail = head; } else { node.prev = t; //设置成尾节点 if (compareAndSetTail(t, node)) { t.next = node; return t; } } } } final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; for (;;) { //获取前驱结点 final Node p = node.predecessor(); //作为第二节点,可以尝试获取锁 if (p == head && tryAcquire(arg)) { //获取锁成功 setHead(node);//将当前节点设置为头节点 p.next = null;// help GC failed = false;//更新failed return interrupted;//返回是否被中断过 } //锁未获取后,判断是否可以被挂起 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }
到现在有点走晕了,先捋一捋
- 通过
lock
获取锁,如果获取失败,则进入acquire
方法尝试再次获取。 acquire
中通过!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
进入两个方法,第一个是尝试第二次尝试获取锁,如果失败再次看第二个方法。addWaiter
将当前线程包装成结点加入同步队列。acquireQueued
方法addWaiter
返回、已入队的结点作为参数,然后第三次尝试获取锁(获取的条件是成为二号结点)。- 最后通过
shouldParkAfterFailedAcquire
和parkAndCheckInterrupt
判断是否需要被挂起。
此时我们再看shouldParkAfterFailedAcquire
和parkAndCheckInterrupt
如何实现的
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { //前驱结点的状态 int ws = pred.waitStatus; //如果前驱结点可以符合条件(可以唤醒当前线程) if (ws == Node.SIGNAL) return true; //如果不符合条件,则一直向前遍历,找到可以唤醒自己的结点 if (ws > 0) { do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { //如果都没有符合条件的结点,那就通过CAS条件将前驱结点的设置为signal compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; }
//挂起当前线程,返回线程中断状态并重置 private final boolean parkAndCheckInterrupt() { LockSupport.park(this); return Thread.interrupted(); }
这两个方法执行完成后,就代表着线程已经被挂起了,只能等待signal标志的线程来唤醒它。
static void selfInterrupt() { //中断当前线程 Thread.currentThread().interrupt(); }
最后回到selfInterrupt
方法。
4. 通过ReentrantLock来了解AQS
首先明白其调用过程很重要
先以非公平锁举例子:
4.1. lock()
final void lock() { /** 上来先抢锁 */ if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); /** 抢锁失败 */ else acquire(1); }
4.2. acquire(int arg)
public final void acquire(int arg) { /** arg = 1 */ if (!tryAcquire(arg) && /** 尝试获取锁 */ acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) /** 加入同步队列、在等待队列中排队拿资源 */ selfInterrupt(); }
此方法是独占模式下线程获取共享资源的顶层入口。如果获取到资源,线程直接返回,否则进入等待队列,直到获取到资源为止,且整个过程忽略中断的影响。这也正是lock()的语义,当然不仅仅只限于lock()
。获取到资源后,线程就可以去执行其临界区代码了。
4.2.1. tryAcquire(int arg)
protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); }
tryAcquire
是在NonfairSync
中实现的,其中调用的是父类Sync
中实现的nonfairTryAcquire
方法。
4.2.1.1. nonfairTryAcquire(int acquires)
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; }
此方法尝试去获取独占资源。如果获取成功,则直接返回true,否则直接返回false。这也正是tryLock()的语义,还是那句话,当然不仅仅只限于tryLock()。
4.2.2. addWaiter(Node mode)
private Node addWaiter(Node mode) { /** 为当前线程创建一个节点 */ Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure Node pred = tail; /** 尝试快速方式直接放到队尾 */ if (pred != null) { node.prev = pred; /** 是否能连接成功 */ if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } /** 快速如队尾失败 则走enq方法 */ enq(node); return node; }
此方法用于将当前线程加入到等待队列的队尾,并返回当前线程所在的结点。
4.2.2.1. enq(Node node)
private Node enq(final Node node) { /** 自旋 放入队尾 */ for (;;) { Node t = tail; if (t == null) { /** 队列为空 创建一个空的标志结点作为head结点 并将tail也指向它 */ if (compareAndSetHead(new Node())) tail = head; } else { /** 正常流程,放入队尾 */ node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }
CAS自旋volatile变量,是一种很经典的用法。
4.2.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(); /** 前驱节点是头节点(说明是二号节点) 且 尝试获取锁(头节点可能释放资源) */ /** 可能被唤醒的、也可能是被中断奥的 */ if (p == head && tryAcquire(arg)) { /** 拿到资源后 将head指向该结点 所以head所指的标杆结点*/ /** 就是当前获取到资源的那个结点或null */ setHead(node); /** setHead 中 已经将 node.pred 设置为 null */ /** 就是为了方便GC收集该节点 也就是 拿完资源的节点需要出队列了 */ p.next = null; // help GC /** 成功获取资源 */ failed = false; /** 返回在等待期间是否被中断过 */ return interrupted; } if (shouldParkAfterFailedAcquire(p, node) && /** 判断自己是否可以休息 */ parkAndCheckInterrupt()) /** 如果可以休息 LockSupport.park() 进入 waiting状态 如果不可中断的情况下被中断了,那么会从park()中醒过来,发现拿不到资源,从而继续进入park()等待。*/ /** 如果等待过程中被中断过,哪怕只有那么一次,就将interrupted标记为true */ interrupted = true; } } finally { /** 如果等待过程中没有成功获取资源(如timeout,或者可中断的情况下被中断了), 那么取消结点在队列中的等待。 */ if (failed) cancelAcquire(node); } }
到这里我们先进入shouldParkAfterFailedAcquire
和 parkAndCheckInterrupt
看看具体操作。
4.2.4.1. shouldParkAfterFailedAcquire(Node pred , Node node)
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; /** 前驱节点的状态*/ if (ws == Node.SIGNAL) /* * 如果已经告诉前驱拿完号后通知自己一下,那就可以安心休息了 */ return true; if (ws > 0) { /* * 如果前驱放弃了,那就一直往前找,直到找到最近一个正常等待的状态,并排在它的后边。 * 注意:那些放弃的结点,由于被“加塞”,它们相当于形成一个无引用链,稍后就会被GC回收 */ do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { /* * 如果前驱正常,那就把前驱的状态设置成SIGNAL,告诉它拿完号后通知自己一下。 * 有可能失败,人家说不定刚刚释放完呢 */ compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; }
如果前驱节点不是SIGNAL
,则不能安心休息,需要找到离自己最近的正常节点,然后接在它后面。
4.2.4.2. parkAndCheckInterrupt()
private final boolean parkAndCheckInterrupt() { LockSupport.park(this); /** Thread.interrupted()会清除当前线程的中断标记位 */ return Thread.interrupted(); }
让线程安心的进入等待状态。park()会让当前线程进入waiting状态。在此状态下,有两种途径可以唤醒该线程:被unpark()、被interrupt()。需要注意的是,Thread.interrupted()会清除当前线程的中断标记位。
这里插入一下线程的状态转换:
4.2.4. selfInterrupt()
static void selfInterrupt() { Thread.currentThread().interrupt(); }
当前线程进行中断。
4.2.5 acquire小结
依旧是这个图片,lock的全过程。
lock -> acquire -> tryAcquire -> addWaiter -> enq -> acquireQueued -> shouldParkAfterFailedAcquire -> parkAndCheckInterrupt -> setHead -> 拿到锁、进行业务代码
4.3. unlock()
加锁搞定了,下面开始解锁 - unlock。还是独占模式下释放共享资源。
public void unlock() { sync.release(1); }
unlock算是解锁的顶级入口,其中调用的还是AQS中的release
方法
4.4. release(int arg)
加锁搞定了,下面开始解锁 - release 了。还是独占模式下释放共享资源,并且唤醒等待队列其他线程来获取共享资源。
public final boolean release(int arg) { if (tryRelease(arg)) { /** 找到头节点 */ Node h = head; if (h != null && h.waitStatus != 0) /** 唤醒等待队列中的下一个线程 */ unparkSuccessor(h); return true; } return false; }
逻辑并不复杂。它调用tryRelease()来释放资源。有一点需要注意的是,它是根据tryRelease()的返回值来判断该线程是否已经完成释放掉资源了!所以自定义同步器在设计tryRelease()的时候要明确这一点。
4.4.1. tryRelease(int arg)
尝试释放指定量的资源。这个方法是交由子类来实现的。在ReentrantLock
实现的。
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; /** 独占线程设置为 null */ setExclusiveOwnerThread(null); } /** 设置为释放后的状态 可能不为0 */ setState(c); return free; }
跟tryAcquire()一样,这个方法是需要独占模式的自定义同步器去实现的。正常来说,tryRelease()都会成功的,因为这是独占模式,该线程来释放资源,那么它肯定已经拿到独占资源了,直接减掉相应量的资源即可(state-=arg),也不需要考虑线程安全的问题。但要注意它的返回值,上面已经提到了,release()是根据tryRelease()的返回值来判断该线程是否已经完成释放掉资源了!所以自义定同步器在实现时,如果已经彻底释放资源(state=0),要返回true,否则返回false。
4.4.2. unparkSuccessor(Node node)
private void unparkSuccessor(Node node) { /** node一般为当前线程所在的结点 */ int ws = node.waitStatus; /** 置零当前线程所在的结点状态 允许失败 */ if (ws < 0) compareAndSetWaitStatus(node, ws, 0); /** 找到下一个需要唤醒的结点s */ Node s = node.next; /** 如果为空或已取消 */ if (s == null || s.waitStatus > 0) { s = null; /** 从后往前找 */ for (Node t = tail; t != null && t != node; t = t.prev) /** waitStatus <= 0 都是有效节点 */ if (t.waitStatus <= 0) s = t; } if (s != null) /** 唤醒下一个节点 */ LockSupport.unpark(s.thread); }
这个函数并不复杂。一句话概括:用unpark()唤醒等待队列中最前边的那个未放弃线程,这里我们也用s来表示吧。此时,再和acquireQueued()联系起来,s被唤醒后,进入if (p == head && tryAcquire(arg))的判断(即使p!=head也没关系,它会再进入shouldParkAfterFailedAcquire()寻找一个安全点。这里既然s已经是等待队列中最前边的那个未放弃线程了,那么通过shouldParkAfterFailedAcquire()的调整,s也必然会跑到head的next结点,下一次自旋p==head就成立啦),然后s把自己设置成head标杆结点,表示自己已经获取到资源了,acquire()也返回了!!And then, DO what you WANT!
4.4.3. release总结
release()是独占模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果彻底释放了(即state=0),它会唤醒等待队列里的其他线程来获取资源。
问题:如果获取锁的线程在release时异常了,没有unpark队列中的其他结点,这时队列中的其他结点会怎么办?是不是没法再被唤醒了?
- 线程突然死掉了?可以通过thread.stop来停止线程的执行,但该函数的执行条件要严苛的多,而且函数注明是非线程安全的,已经标明Deprecated;
- 线程被interupt了?线程在运行态是不响应中断的,所以也不会抛出异常;
- release代码有bug,抛出异常了?目前来看,Doug Lea的release方法还是比较健壮的,没有看出能引发异常的情形(如果有,恐怕早被用户吐槽了)。除非自己写的tryRelease()有bug,那就没啥说的,自己写的bug只能自己含着泪去承受了。
第四部分原文:
4.5. MyLock
仿照ReentrantLock
来实现一个MyLock
import java.util.concurrent.locks.AbstractQueuedSynchronizer; /** * @author gyh * @csdn https://blog.csdn.net/qq_40788718 * @date 2020/6/4 10:15 */ public class MyLock extends AbstractQueuedSynchronizer { public void lock() { this.acquire(1); } public void unlock() { this.release(1); } @Override protected boolean tryAcquire(int arg) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0){ if (compareAndSetState(0, 1)) { setExclusiveOwnerThread(current); return true; } }else if (current == getExclusiveOwnerThread()){ setState(1); return true ; } return false ; } @Override protected boolean tryRelease(int arg) { if (getExclusiveOwnerThread() != Thread.currentThread()) throw new IllegalMonitorStateException() ; setExclusiveOwnerThread(null); setState(0); return true ; } }
import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * @author gyh * @csdn https://blog.csdn.net/qq_40788718 * @date 2020/6/4 13:59 */ public class MyLockDemo { static int count = 0 ; public static void main(String[] args) throws InterruptedException { final MyLock lock = new MyLock() ; ExecutorService executorsService = Executors.newCachedThreadPool() ; CountDownLatch countDownLatch = new CountDownLatch(1000) ; for (int i=0 ; i<1000 ; i++){ executorsService.execute(()->{ try{ lock.lock(); count++ ; System.out.println("线程:"+Thread.currentThread().getName()+" 数值:"+count); }catch (Exception e){ e.printStackTrace(); }finally { lock.unlock(); countDownLatch.countDown(); } }); } countDownLatch.await(); executorsService.shutdown(); System.out.println(count); } }
测试了几次,感觉成功了,如果有问题请指出来。
4. Condition
Condition
是一个接口,从1.5的时候出现的,是用来替代Object
的wait
、notify
。所以显而易见,Condition
的await
和signal
肯定效率更高、安全性更好。Condition
是依赖于lock
实现的。并且await
和signal
只能在lock
的保护之内使用。
sychronized + Object.wait = Lock + Condition
package java.util.concurrent.locks; import java.util.concurrent.TimeUnit; import java.util.Date; public interface Condition { //导致当前线程等到发信号或 interrupted 。 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(); }
(网图)
从图片中可以看出AQS的整体框架的大致轮廓,ConditionObject
是Condition
的实现类。
/** First node of condition queue. */ private transient Node firstWaiter; /** Last node of condition queue. */ private transient Node lastWaiter;
可以看出这是队列的头和尾。
看一下两个最常用的方法
ConditionObject.await
public final void await() throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); //1 Node node = addConditionWaiter(); //2 int savedState = fullyRelease(node); int interruptMode = 0; //3 while (!isOnSyncQueue(node)) { LockSupport.park(this); if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) break; } //4 if (acquireQueued(node, savedState) && interruptMode != THROW_IE) interruptMode = REINTERRUPT; if (node.nextWaiter != null) // clean up if cancelled unlinkCancelledWaiters(); if (interruptMode != 0) reportInterruptAfterWait(interruptMode); }
- 把当前线程的节点加入到等待队列中
- 由于调用await()方法的线程是已经获取了锁的,所以在加入到等待队列之后,需要去释放锁,并且唤醒后继节点线程
- 挂起当前线程,当别的线程调用了signal(),并且是当前线程被唤醒的时候才从park()方法返回
- 当被唤醒后,该线程会尝试去获取锁,只有获取到了才会从await()方法返回,否则的话,会挂起自己
ConditionObject.signal
public final void signal() { if (!isHeldExclusively()) throw new IllegalMonitorStateException(); Node first = firstWaiter; if (first != null) doSignal(first); }
从first开始遍历等待队列,把第一个非空、没取消的节点transfer到同步队列
5. 同步队列与等待队列
AQS中存有同步队列,而Condition中存有等待队列。他们二者有什么区别?
同步队列存放着竞争同步资源的线程的引用;等待队列存放着待唤醒的线程的引用。
同步队列中存放着一个个节点,当线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成为一个节点并将其加入同步队列,首节点表示的获取同步状态成功的线程节点。
Condition维护着一个等待队列,与同步队列相似。主要针对await和signal的操作。
借助博客:https://blog.csdn.net/MakeContral/article/details/78135531 的例子可能更好理解
public class CondtionTest { public static class ThreadA extends Thread{ @Override public void run(){ try{ lock.lock(); for (int i=0 ;i<3;i++){ System.out.println("A进程输出" + " : " + ++index); conditionB.signal(); conditionA.await(); } }catch (Exception e){ e.printStackTrace(); }finally { lock.unlock(); } } } public static class ThreadB extends Thread{ @Override public void run(){ try{ lock.lock(); for (int i=0 ;i<3;i++){ System.out.println("B进程输出" + " : " + ++index); conditionC.signal(); conditionB.await(); } }catch (Exception e){ e.printStackTrace(); }finally { lock.unlock(); } } } public static class ThreadC extends Thread{ @Override public void run(){ try{ lock.lock(); for (int i=0 ;i<3;i++){ System.out.println("C进程输出" + " : " + ++index); conditionA.signal(); conditionC.await(); } }catch (Exception e){ e.printStackTrace(); }finally { lock.unlock(); } } } public static ReentrantLock lock = new ReentrantLock(); public static Condition conditionA = lock.newCondition(); public static Condition conditionB = lock.newCondition(); public static Condition conditionC = lock.newCondition(); public static int index = 0; public static void main(String[] args){ ThreadA threadA = new ThreadA(); ThreadB threadB = new ThreadB(); ThreadC threadC = new ThreadC(); threadA.start();//(1) threadB.start();//(2) threadC.start();//(3) } }
输出结果为:
这种情况是B线程先占到了锁了。
参考:
https://blog.csdn.net/m47838704/article/details/80013056
https://blog.csdn.net/zhangdong2012/article/details/81151661
https://blog.csdn.net/qq_28605513/article/details/84194624
https://blog.csdn.net/fuyuwei2015/article/details/83719444
https://www.cnblogs.com/yanlong300/p/9772271.html
https://blog.csdn.net/andyzhaojianhui/article/details/79361454