java condition_Java并发包之AQS框架Condition源码解析

前几篇文章对AQS的独占资源模式和共享资源模式的源码进行了解析,本篇文章来分析AQS最后一个非常重要的知识点:Condition源码解析。此篇文章内容较长,我是一个字一个字敲出来的,请您也慢慢的品读,自己认识有一定的局限性,欢迎交流更正。

大家还记得Java中怎样实现生产者和消费者模式吗?原理是非常的简单的,为了防止生产者和消费者不均衡的情况发生,在生产者/消费者模式中会提供一个缓冲区,而这个缓冲区的作用就是存储数据的,生产者生产数据后存放在缓冲区中,而消费者从缓冲区中消费这些数据,从而使生产者和消费者进行了解耦,简单概括如下:

1:生产者生产数据,直到缓冲区满后阻塞。2:消费者消费数据,直到缓冲区为空后阻塞。3:当缓冲区有数据后,会通知消费者进行消费数据4:当缓冲区没有数据后,会通知生产者进行生产数据5:可以同时存在多个生产者和多个消费者

对于上面生产者和消费者模式的实现方式我们首先想到的就是通过调用Object中的wait()方法阻塞,调用notify()和notifyAll()方法进行通知,但是调用上述方法的前提是必须获取锁,所以可以利用synchronized+wait()+notify()/notifyAll()方式来实现生产者和消费者模式。下面举一个简单的例子来说明。

缓冲区的代码如下:

/** * 缓冲区 */public class SyncCache { //缓冲区 private String[] data = new String[10]; //缓冲区数组索引 private int index; //生产数据 public void product(String productData) { synchronized (this) { if (index == data.length) { try { System.out.println("缓冲区已满,生产者被阻塞"); this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } data[index] = productData; index++; this.notify(); } } //消费数据 public String consume() { synchronized (this) { if (index == 0) { try { System.out.println("缓冲区已空,消费者被阻塞"); this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } this.notify(); index--; String consumeData = data[index]; return consumeData; } }}

生产者代码如下:

public class Product implements Runnable { private SyncCache cache; public Product(SyncCache cache) { this.cache = cache; } @Override public void run() { int count = 1; for(;;){ String data = "a"+count; cache.product(data); System.out.println("生产者生产了数据:"+data); count++; } }}

消费者代码如下:

public class Consume implements Runnable { private SyncCache cache; public Consume(SyncCache cache) { this.cache = cache; } @Override public void run() { for(;;){ System.out.println("消费者消费了数据:" + cache.consume()); } }}测试代码如下:public class Test { public static void main(String[] args) { SyncCache cache = new SyncCache(); Product product = new Product(cache); Consume consume = new Consume(cache); new Thread(product).start(); new Thread(consume).start(); }}

通过上面简单的例子运行结果如下:

ff8b1510298a84048bfb884033c0f493.png

上面这个例子非常的简单,但是能够很好的说明生产者和消费者模式的例子,可以看出调用Object对象的wait()方法和notify()方法必须在获取锁的前提下,并且这个锁属于独占锁

上面说了这么多,那么和要讲的Condition有什么联系吗?上面通过调用wait()/notify()只是实现生产者和消费者模式的一种方法,Condition也能实现生产者和消费者模式。

1:synchronized通过和AQS实现的锁对应,比如:ReentrantLock2:Condition中的await()方法和Object中的wait()方法对应3:Condition中的signal()方法和Object的notify()方法对应4:Condition中的signalAll()方法和Object的notifyAll()方法对应

通过上面的讲述大家明白了Condition的作用了吧,但是Condition只是一个接口,在AQS内部有它的一个实现类ConditionObject,接下来我从源码角度去分析AQS是怎样实现Condition的。

ConditionObject是AQS中的内部类,它复用了AQS中的Node节点的定义,如图:

e9ff4b4b0d61f56b6103ba97a8f793f6.png

在ConditionObject中也有一个以Node为节点的队列,我们称之为Condition队列,在AQS中有一个CLH等待队列,我们称之为Sync队列,但是两者还是有一定的区别的。

1:sync队列的头结点为head,而Condition队列的头结点为firstWaiter2:sync队列的尾节点为tail,而Condition队列的尾节点为lastWaiter3:sync队列的head没有和任何线程绑定,而Condition队列的firstWaiter绑定了线程

一、await在条件变量上的等待

和Object中的wait()方法一样,在调用await()方法之前必须获取到锁,并且是独占锁

public final void await() throws InterruptedException {//判断线程是否被中断,如果被中断则抛出异常 if (Thread.interrupted()) throw new InterruptedException();//$1:将当前线程封装成Node节点 Node node = addConditionWaiter();//$2:调用await()后要释放锁,这一步就是释放获取的资源 int savedState = fullyRelease(node); int interruptMode = 0;//$3:通过while循环监测线程状态,直到线程被signal或者线程被中断并且放入了synch队列中 while (!isOnSyncQueue(node)) {//阻塞当前线程 LockSupport.park(this);//$4:如果发生中断则退出while循环,判断中断模式,如果中断是抛出异常,还是稍后在处理中断 if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) break; }//退出上述的while循环后,通过调用acquireQueued()方法获取资源,如果获取资源时发生了中断,如果在调用await()方法时被中断,则依然是THROW_IE if (acquireQueued(node, savedState) && interruptMode != THROW_IE) interruptMode = REINTERRUPT;//如果是因为中断,此时waitStatus=0,但是此时它仍在Condition队列中,所以需要从Condition队列中清除 if (node.nextWaiter != null) // clean up if cancelled unlinkCancelledWaiters();//$5:如果有中断,则处理不同的中断 if (interruptMode != 0) reportInterruptAfterWait(interruptMode);}

通过对源码的注释,大家对await有一定的了解了。我系统的总结以下:

第一步:首先判断当前线程是中断,如果被中断,则抛出异常,如果没有被中断,则继续下面的流程。第二步:通过调用addConditionWaiter()将当前线程封装成Node节点存放到Condition队列的尾部第三步:因为当前线程已经获取了锁,所以调用await需要释放资源,所以通过调用fullyRelease()释放资源,也就是释放锁,因为这个锁是独占锁并且可以重入,所以要全部把资源释放,从fully字面上也可以理解。第四步:通过while循环判断当前线程是否在sync等待队列上,如果没有在sync队列上,则需要阻塞当前线程,然后调用checkInterruptWaiting()方法判断是否被中断过,如果被中断过,则跳出while循环。第五步:通过调用acquireQueued()方法获取资源,如果在调用这个方法时被中断,则中断类型变成REINTERRUPT(稍后处理中断),我们知道这个方法返回值只是记录是否被中断过,并不会响应中断。第六步:如果是因为中断,此时waitStatus=0,但是此时它仍在Condition队列中,所以需要从Condition队列中清除第七步:如果被中断,则调用reportInterruptAfterWait()方法处理不同的中断类型

从上面可以看出await()会释放当前获取的锁,然后阻塞,直到被signal或者被中断。流程图如下:

b3bf17269a303ee03b7537747cf33e0c.png

$1:addConditionWaiter():将线程添加到Condition队列中

private Node addConditionWaiter() {//获取condition队列的尾节点 Node t = lastWaiter; // 如果尾节点被取消,则从condition中清除 if (t != null && t.waitStatus != Node.CONDITION) { unlinkCancelledWaiters(); t = lastWaiter; }//将当前线程封装成Node节点,并且waitStatus状态为CONDITION Node node = new Node(Thread.currentThread(), Node.CONDITION); if (t == null) firstWaiter = node; else t.nextWaiter = node; lastWaiter = node; return node;}

上述这个方法就是将当前线程封装成Node节点,然后加入到Condition的尾部,在加入之前需要检查以下尾部节点t是否还在等待Condition条件,如果被signal或者被中断,则调用清除方法将尾节点从Condition队列中清除掉。

$2:fullyRelease():释放已经获取的资源

final int fullyRelease(Node node) {//这个failed标识:判断资源是否释放成功 boolean failed = true; try {//获取到当前资源的值 int savedState = getState();//调用释放资源方法 if (release(savedState)) { failed = false; return savedState; } else { throw new IllegalMonitorStateException(); } } finally {//如果释放资源失败,则节点的状态改成CANCELED if (failed) node.waitStatus = Node.CANCELLED; }}

这个方法非常的简单,就是释放拥有的资源,如果资源释放失败,则将当前线程节点的状态变成CANCELLED。

$3:isOnSyncQueue():判断节点是否在sync队列中

final boolean isOnSyncQueue(Node node) {//如果状态是CONDITION:说明在condition队列上,一定不在sync队列上//如果当前节点的前驱节点为空:说明一定不在sync队列上 if (node.waitStatus == Node.CONDITION || node.prev == null) return false;//如果当前节点的后继节点不为空,说明一定在sync队列上 if (node.next != null) return true; return findNodeFromTail(node);}private boolean findNodeFromTail(Node node) { Node t = tail; for (;;) { if (t == node) return true; if (t == null) return false; t = t.prev; }}

我首先贴一张加入sync队列的源码图:

702c23b069aa4de7804d2bf0cb1ef7ec.png

上图中的第1步就是设置node.prev为当前的尾节点,如果node.prev==null,则可以判定当前节点一定不在sync上。在往condition队列添加节点时,我们并没有设置节点的后继节点next的值,而在sync队列中我们设置过node.next,所以如果node.next!=null则说明一定在sync节点上。

如果上面条件都不符合,也就是说node.prev不一定为null,node.next一定为null,这个时候可能节点正在加入sync队列,不过可能加入失败,如上图第2步通过CAS可能会失败,所以在尝试通过调用findNodeFromTail()判断以下是否加入sync队列成功。这个方法从尾部开始遍历sync队列,如果监测到节点则直接返回true,如果此时还没有成功,则不在等待了,直接返回false

$4:checkInterruptWhileWaiting():监测是否发生中断,如果发生中断,则判断不同的中断处理类型

private static final int REINTERRUPT = 1;private static final int THROW_IE = -1;private int checkInterruptWhileWaiting(Node node) {//如果发生中断,则调用transferAfterCancelledWait()方法判断//如果没有发生中断,则直接返回0 return Thread.interrupted() ? (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) : 0;}

从await()方法可以看出,线程从park中返回主要有两个条件,

第一个条件:被signal/signalAll唤醒的

第二个条件:线程被中断

如果是因为第二个条件线程被中断返回的,则主要返回3个值:

0:表示没有发生中断

REINTERRUPT(1):表示发生了中断,在返回之前即使发生中断但是被忽略,等待返回后重新处理中断

THROW_IE(-1):表示发生了中断,后续需要抛出异常处理

final boolean transferAfterCancelledWait(Node node) {//$1:如果线程中断则通过CAS将waitStatus从CONDITION变成0 if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {//成功后加入到sync队列中等待获取资源 enq(node); return true; }  while (!isOnSyncQueue(node)) Thread.yield(); return false;}

这个方法在AQS框架中,不在ConditionObject中,此方法虽然简短,但是蕴含着很多的知识点。当是第一个条件被signal/signalAll唤醒返回的话,它首先会将状态从CONDITION变成0,

1:如果$1的CAS执行成功,则说明中断先于signal发生,因为如果signal先发生,它会把状态从CONDITION变成0.

2:如果1执行成功,则说明从中断中返回,调用end()将线程加入synch队列,并返回true

3:如果1执行失败,说明signal()已经执行,但是有可能没有执行完成,则判断节点是否在sync队列中,如果不在sync队列中则调用yield让出CPU,while是一个自旋,直到节点加入到sync队列才返回。

上面就是await()方法的所有内容,相信大家有一定的理解了,其实用一句话总结:await就是将当前线程封装成Node存放在Condition队列中,然后释放已经获取的资源,直到被signal/signalAll或者线程被中断才从Condition队列移除。

awaitNanos、awaitUntil和await的机制类似,大家可以自行观看代码。

$5:reportInterruptAfterWait():如果线程在阻塞时被中断,则需要对中断处理

private void reportInterruptAfterWait(int interruptMode) throws InterruptedException { if (interruptMode == THROW_IE) throw new InterruptedException(); else if (interruptMode == REINTERRUPT) selfInterrupt();}

方法很简单,如果中断的类型是THROW_IE,则直接抛出异常,如果是REINTERRUPT,则处理中断。

二、signal唤醒在Condition队列的头结点firstWaiter对应的线程

上面分析了已经获取资源的线程因为调用了await,则需要把线程加入到condition队列,然后释放资源的过程,我也说了,从阻塞中返回要么调用signal/signalAll,要么线程被中断,接下来继续分析signal

public final void signal() {//如果不是独占模式,则直接抛出异常 if (!isHeldExclusively()) throw new IllegalMonitorStateException();//获取condition队列中第一个节点 Node first = firstWaiter; if (first != null)//唤醒condition队列的第一个节点 doSignal(first);}

这个方法首先判断是否是独占模式,如果不是则直接抛出异常,如果是则获取condition队列中的第一个节点,然后唤醒第一个节点,而唤醒的工作交给了doSignal(),这里所说的唤醒:将线程从condition队列迁移到sync队列中,然后通过独占模式原理获取资源

private void doSignal(Node first) { do { if ( (firstWaiter = first.nextWaiter) == null) lastWaiter = null; first.nextWaiter = null; } while (!transferForSignal(first) && (first = firstWaiter) != null);}

这个方法首先把第一个节点从condition队列中移除,然后调用transferFOrSignal()去改变节点的状态waitStatus,这个改变状态可能会失败,因为线程已经过时了或者被中断了。do{}while()循环直到被唤醒成功为止。

final boolean transferForSignal(Node node) { if (!compareAndSetWaitStatus(node, Node.CONDITION, 0)) return false; //如果上述CAS成功,则将节点从condition队列转移到sync队列的尾部 Node p = enq(node); int ws = p.waitStatus; if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) LockSupport.unpark(node.thread); return true;}

这个方法首先通过CAS将节点的waitStatus从CONDITION变成0,如果失败了,直接返回false,如果成功则直接将节点加入到sync队列等待获取资源,enq()方法返回的是node的前驱节点,如果前驱节点不是SIGNAL,则需要调用unpark()方法唤醒被await阻塞的线程,被await阻塞的线程将继续执行await()方法中LockSupport.park()后面的代码。

72af8a2ec0440be736672b6cd793f9c5.png

上面就是Condition的全部内容,大家可以仔细的读一下源码,会有一个清晰的任务,对Object中wait()和notify()机制熟悉,那么Condition的await()和signal()是它的另一种实现方式,机制理解了,看源码就非常的简单了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值