难以理解的AQS(下)
在上一篇博客,简单的说下了AQS的基本概念,核心源码解析,但是还有一部分内容没有涉及到,就是AQS对条件变量的支持,这篇博客将着重介绍这方面的内容。
条件变量
基本应用
我们先通过模拟一个消费者/生产者模型来看下条件变量的基本应用:
- 当有数据的时候,生产者停止生产数据,通知消费者消费数据;
- 当没有数据的时候,消费者停止消费数据,通知生产者生产数据;
public class CommonResource {
private boolean isHaveData = false;
Lock lock = new ReentrantLock();
Condition producer_con = lock.newCondition();
Condition consumer_con = lock.newCondition();
public void product() {
lock.lock();
try {
while (isHaveData) {
try {
System.out.println("还有数据,等待消费数据");
producer_con.await();
} catch (InterruptedException e) {
}
}
System.out.println("生产者生产数据了");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
isHaveData = true;
consumer_con.signal();
} finally {
lock.unlock();
}
}
public void consume() {
lock.lock();
try {
while (!isHaveData) {
try {
System.out.println("没有数据了,等待生产者消费数据");
consumer_con.await();
} catch (InterruptedException e) {
}
}
System.out.println("消费者消费数据");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
isHaveData = false;
producer_con.signal();
} finally {
lock.unlock();
}
}
}
public class Main {
public static void main(String[] args) {
CommonResource resource = new CommonResource();
new Thread(() -> {
while (true) {
resource.product();
}
}).start();
new Thread(() -> {
while (true) {
resource.consume();
}
}).start();
}
}
运行结果:
这就是条件变量的应用,第一反应是不是和object中的wait/nofity很像,wait/nofity是配合synchronized工作的,而条件变量的await/signal是配合使用AQS实现的锁
来完成工作的,当然也要看用AQS实现的锁是否支持了条件变量。synchronized只能与一个共享变量进行工作,而AQS实现的锁支持多个条件变量。
我们试着分析下上面的代码:
首先创建了两个条件变量,一个条件变量用来阻塞/唤醒消费者线程,一个条件变量用来阻塞/唤醒生产者线程。
生产者,首先获取了独占锁,判断是否有数据:
- 如果有数据,则调用条件变量producer_con的await方法,阻塞当前线程,当消费者线程再次调用该条件变量producer_con的signal方法,就会唤醒该线程。
- 如果没有数据,则生产数据,并且调用条件变量consumer_con的signal方法,唤醒因为调用consumer_con的await方法而被阻塞的消费者线程。
最终释放锁。
消费者,首先获取了独占锁,判断是否有数据:
- 如果没有数据,则调用条件变量consumer_con的await方法,阻塞当前线程,当生产者线程再次调用该条件变量consumer_con的signal方法,就会唤醒该线程。
- 如果有数据,则消费数据,并且调用条件变量producer_con的signal方法,唤醒因为调用producer_con的await方法而被阻塞的生产者线程。
最终释放锁。
这里有一点需要特别注意:
- 释放锁,一般应该放在finally里面,以防中间出现异常,锁没有被释放。
为了加深对条件变量的理解,我们再来看一个例子,两个线程交替打印奇偶数:
public class Test {
private int num = 0;
private Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void add() {
while(num<100) {
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + ":" + num++);
condition.signal();
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
public class Main {
public static void main(String[] args) {
Test test=new Test();
new Thread(() -> {
test.add();
}).start();
new Thread(() -> {
test.add();
}).start();
}
}
运行结果:
翻阅网上的大多数案例是分两个线程方法交替打印,同时开两个条件变量,其中一个条件变量负责阻塞/唤醒打印奇数的线程,一个变量负责阻塞/唤醒打印偶数的线程,但是个人觉得没什么必要,两个线程共用一个线程方法,共用一个条件变量也可以。不知道各位看官是什么想的?
源码解析
当我们点开lock.newCondition,发现它有好几个实现类:
我们选择ReentrantLock的实现类,实际上其他实现类也是相同的,只是为了和上面案例中的对应起来,所以先选择ReentrantLock的实现类:
public Condition newCondition() {
return sync.newCondition();
}
继续往下点:
final ConditionObject newCondition() {
return new ConditionObject();
}
可以看到,当我们调用lock.newnewCondition,最终会new出一个ConditionObject对象,而ConditionObject类是AbstractQueuedSynchronizer的内部类,我们先看下ConditionObject的UML图:
其中firstWaiter保存的是该条件变量下条件队列的首节点,lastWaiter保存的是该条件变量下条件队列的尾节点。这里只保存了条件队列的首节点和尾节点,中间的节点保存在哪里呢? 让我们点开await方法:
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter();
int savedState = fullyRelease(node);
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
在这里,我们就搞清楚三个问题即可:
- 完整的条件队列保存在哪里,以什么方式保存?
- await方法,是如何释放锁的?
- await方法,是如何阻塞线程的?
第一个问题在addConditionWaiter方法可以得到答案:
private Node addConditionWaiter() {
Node t = lastWaiter;
// If lastWaiter is cancelled, clean out.
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
首先是判断条件队列中的尾节点是否被取消了,如果被取消了,执行unlinkCancelledWaiters方法。我们这里肯定没有被取消,事实上,如果是第一次调用await方法,lastWaiter是为空的,所以肯定不会进入第一个if。随后,新建一个Node,这个Node类就是上一篇博客中大量介绍过的,也是AbstractQueuedSynchronizer的内部类,也就是新建了一个Node节点,其中保存了当前线程和Node的类型,这里Node的类型是CONDITION,如果t==null,则说明新建的Node是第一个节点,所以赋值给firstWaiter ,否则将尾节点的nextWaiter设置为新Node,形成一个单向链表,这个nextWaiter在哪里呢,它是通过node点出来的,也就是它也属于node类的一个字段:
这说明了一个比较重要的问题:
AQS的阻塞队列是以双向的链表的形式保存的,是通过prev和next建立起关系的,但是AQS中的条件队列是以单向链表的形式保存的,是通过nextWaiter建立起关系的,也就是AQS的阻塞队列和AQS中的条件队列并非同一个队列。
第一个问题解决了,我们再来看第二个问题,第二个问题答案在await的第二个方法:
final int fullyRelease(Node node) {
boolean failed = true;
try {
int savedState = getState();
if (release(savedState)) {
failed = false;
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
首先调用getState方法,这个state是什么,不知大家是否还有印象,对于ReentrantLock来说,state就是重入次数,随后调用release方法,传入state。也就是不管重入了多少次,这里是一次性把锁完全释放掉。
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方法,这个方法正是需要被重写的。
当完成了前两个方法的调用后,就会进行一个判断isOnSyncQueue,一般来说会进入这个if,park这个线程,等待唤醒,这就解决了第三个问题。
下面我们再来看看signal方法,同样的,我们需要解决几个问题:
- AQS的条件队列和阻塞队列既然不是同一个队列,那么是不是被await的线程永远不会进入阻塞队列?
- signal方法是如何唤醒线程的?
public final void signal() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
重点在于doSignal中的transferForSignal方法:
final boolean transferForSignal(Node node) {
/*
* If cannot change waitStatus, the node has been cancelled.
*/
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
/*
* Splice onto queue and try to set waitStatus of predecessor to
* indicate that thread is (probably) waiting. If cancelled or
* attempt to set waitStatus fails, wake up to resync (in which
* case the waitStatus can be transiently and harmlessly wrong).
*/
Node p = enq(node);
int ws = p.waitStatus;
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
在这个方法中,我们会调用enq方法,把条件队列的线程放入阻塞队列中,然后调用unpark方法,唤醒线程。
本篇博客到这里也结束了。
经过上下两篇博客,相信大家对AQS一定有了一个比较浅显的理解。聪明的你,可以看出来,其实这两篇博客有很多内容都没有讲透,甚至有点模棱两可,只是“蜻蜓点水”,所以这也符合了我的标题:难以理解的AQS。的确,AQS要深入研究的话,不比线程池简单多少。看,我又再给自己找理由了。希望经过今后的沉淀,我可以把这两篇博客重写下,然后换个标题“彻底理解AQS”,嘿嘿。
难以理解的AQS(下)的更多相关文章
- 难以理解的AQS(上)
在一篇博客中,我们看了下CopyOnWriteArrayList的源码,不是很难,里面用到了一个可重入的排他锁: ReentrantLock,这东西看上去和Synchronized差不多,但是和Syn ...
- RxSwift 系列(九) -- 那些难以理解的概念
前言 看完本系列前面几篇之后,估计大家也还是有点懵逼,本系列前八篇也都是参考RxSwift官方文档和一些概念做的解读.上几篇文章概念性的东西有点多,一时也是很难全部记住,大家脑子里面知道有这么个概念就 ...
- Java的内部类真的那么难以理解?
01 前言 昨天晚上,我把车停好以后就回家了.回家后才发现手机落在车里面了,但外面太冷,冷到骨头都能感受到寒意——实在是不想返回一趟去取了(小区的安保还不错,不用担心被砸车玻璃),于是打定主意过几个小 ...
- 对于新手来说,Python 中有哪些难以理解的概念?
老手都是从新手一路过来的,提起Python中难以理解的概念,可能很多人对于Python变量赋值的机制有些疑惑,不过对于习惯于求根究底的程序员,只有深入理解了某个事物本质,掌握了它的客观规律,才能得心应 ...
- 深入理解Java虚拟机--下
深入理解Java虚拟机--下 参考:https://www.zybuluo.com/jewes/note/57352 第10章 早期(编译期)优化 10.1 概述 Java语言的"编译期&q ...
- 深入理解linux系统下proc文件系统内容
深入理解linux系统下proc文件系统内容 内容摘要:Linux系统上的/proc目录是一种文件系统,即proc文件系统. Linux系统上的/proc目录是一种文件系统,即proc文件系统.与其它 ...
- 通过作用域链解析js函数一些难以理解的的作用域问题
基本原理 js函数在执行时,系统会创建一个隐式的属性scope,scope中存储的是函数的作用域链. 通过对这个scope的分析,就能解释JavaScript中许多难以理解的问题: 例1: funct ...
- 9.深入理解AbstractQueuedSynchronizer(AQS)
1. AQS简介 在上一篇文章中我们对lock和AbstractQueuedSynchronizer(AQS)有了初步的认识.在同步组件的实现中,AQS是核心部分,同步组件的实现者通过使用AQS提供的 ...
- 深入理解AbstractQueuedSynchronizer(AQS)
本人免费整理了Java高级资料,涵盖了Java.Redis.MongoDB.MySQL.Zookeeper.Spring Cloud.Dubbo高并发分布式等教程,一共30G,需要自己领取.传送门:h ...
随机推荐
- MongoDB中级---->关联多表查询
http://www.linuxidc.com/Linux/2011-08/41043.htm DBRef is a more formal specification for creating re ...
- BZOJ_1797_[Ahoi2009]Mincut 最小割_最小割+tarjan
BZOJ_1797_[Ahoi2009]Mincut 最小割_最小割+tarjan Description A,B两个国家正在交战,其中A国的物资运输网中有N个中转站,M条单向道路.设其中第i (1≤ ...
- Java开源生鲜电商平台-通知模块设计与架构(源码可下载)
Java开源生鲜电商平台-通知模块设计与架构(源码可下载) 说明:对于一个生鲜的B2B平台而言,通知对于我们实际的运营而言来讲分为三种方式: 1. 消息推送:(采用极光推送) ...
- hystrix 请求合并(6)
hystrix支持N个请求自动合并为一个请求,这个功能在有网络交互的场景下尤其有用,比如每个请求都要网络访问远程资源,如果把请求合并为一个,将使多次网络交互变成一次,极大节省开销.重要一点,两个请求能 ...
- Android之微信朋友圈UI实现--ExpandableListView+GridView
PS:我们都知道微信,更是知道朋友圈,很多人在朋友圈里卖起了化妆品,打入广告等为自己做一下推广,里面会附带一写好看的图片,上面有标题,有描述,整体布局每场的美观,那么这是怎么实现的呢,有些人可能会单个 ...
- Boosting(提升方法)之XGBoost
XGBoost是一个机器学习味道非常浓厚的模型,在数学上非常规范,运用正则化.L2范数.二阶梯度.泰勒公式和分布式计算方法,对GBDT等提升树模型进行优化,不仅能处理更大规模的数据,而且运行效率特别高 ...
- 《前端之路》之 前端图片 类型 & 优化 & 预加载 & 懒加载 & 骨架屏
目录 09: 前端图片 类型 & 优化 & 预加载 & 懒加载 & 骨架屏 09: 前端图片 类型 & 优化 & 预加载 & 懒加载 & ...
- JavaWeb 乱码问题终极解决方案!
经常有读者在公众号上问 JavaWeb 乱码的问题,昨天又有一个小伙伴问及此事,其实这个问题很简单,但是想要说清楚却并不容易,因为每个人乱码的原因都不一样,给每位小伙伴都把乱码的原因讲一遍也挺费时间的 ...
- Python3|ddt|unittest|浅议数据驱动测试
目录 1.DDT简介 2.data装饰器 3.unpack装饰器 4.file_data装饰器 5.总结 1.DDT简介 Data-Driven Tests(DDT)即数据驱动测试.它允许您通过不同的 ...
- .net core redis 驱动推荐,为什么不使用 StackExchange.Redis
前言 本人从事 .netcore 转型已两年有余,对 .net core 颇有好感,这一切得益于优秀的语法.框架设计. 2006年开始使用 .net 2.0,从 asp.net 到 winform 到 ...