java并发等待条件的实现原理(Condition)
本篇继续学习AQS中的另外一个内容-Condition。想必学过java的都知道Object.wait和Object.notify,同时也应该知晓这两个方法的使用离不开synchronized关键字。
synchronized是jvm级别提供的同步原语,它的实现机制隐藏在jvm实现中。作为Lock系列功能中的Condition,就是用来实现类似 Object.wait和Object.notify 对应功能的。
使用场景
为了更好的理解Lock和Condition的使用场景,下面我们先来实现这样一个功能:有多个生产者,多个消费者,一个产品容器,
我们假设容器最多可以放3个产品,如果满了,生产者需要等待产品被消费,如果没有产品了,消费者需要等待。我们的目标是一共生产10个产品,最终消费10个产品,
如何在多线程环境下完成这一挑战呢?下面是我简单实现的一个demo,仅供参考。
package com.lock.condition.test; import java.util.LinkedList;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock; public class LockConditionTest {
// 生产 和 消费 的最大总数
public static int totalCount = 10;
// 已经生产的产品数
public static volatile int hasProduceCount = 0;
// 已经消费的产品数
public static volatile int hasConsumeCount = 0;
// 容器最大容量
public static int containerSize = 3;
// 使用公平策略的可重入锁,便于观察演示结果
public static ReentrantLock lock = new ReentrantLock(true);
public static Condition notEmpty = lock.newCondition();
public static Condition notFull = lock.newCondition();
// 容器
public static LinkedList<Integer> container = new LinkedList<Integer>();
// 用于标识产品
public static AtomicInteger idGenerator = new AtomicInteger(); public static void main(String[] args) {
Thread p1 = new Thread(new Producer(), "p-1");
Thread p2 = new Thread(new Producer(), "p-2");
Thread p3 = new Thread(new Producer(), "p-3"); Thread c1 = new Thread(new Consumer(), "c-1");
Thread c2 = new Thread(new Consumer(), "c-2");
Thread c3 = new Thread(new Consumer(), "c-3"); c1.start();
c2.start();
c3.start();
p1.start();
p2.start();
p3.start();
try{
c1.join();
c2.join();
c3.join();
p1.join();
p2.join();
p3.join();
}catch(Exception e){ }
System.out.println(" done. ");
}
static class Producer implements Runnable{
@Override
public void run() {
while(true){
lock.lock();
try{
// 容器满了,需要等待非满条件
while(container.size() >= containerSize){
notFull.await();
} // 到这里表明容器未满,但需要再次判断是否已经完成了任务
if(hasProduceCount >= totalCount){
System.out.println(Thread.currentThread().getName()+" producer exit");
return ;
} int product = idGenerator.incrementAndGet();
// 把生产出来的产品放入容器
container.addLast(product);
System.out.println(Thread.currentThread().getName() + " product " + product);
hasProduceCount++; // 通知消费线程可以去消费了
notEmpty.signal();
} catch (InterruptedException e) {
}finally{
lock.unlock();
}
}
}
}
static class Consumer implements Runnable{
@Override
public void run() {
while(true){
lock.lock();
try{
if(hasConsumeCount >= totalCount){
System.out.println(Thread.currentThread().getName()+" consumer exit");
return ;
} // 一直等待有产品了,再继续往下消费
while(container.isEmpty()){
notEmpty.await(2, TimeUnit.SECONDS);
if(hasConsumeCount >= totalCount){
System.out.println(Thread.currentThread().getName()+" consumer exit");
return ;
}
} Integer product = container.removeFirst();
System.out.println(Thread.currentThread().getName() + " consume " + product);
hasConsumeCount++; // 通知生产线程可以继续生产产品了
notFull.signal();
} catch (InterruptedException e) {
}finally{
lock.unlock();
}
}
}
}
}
一次执行的结果如下:
p-1 product 1
p-3 product 2
p-2 product 3
c-3 consume 1
c-2 consume 2
c-1 consume 3
p-1 product 4
p-3 product 5
p-2 product 6
c-3 consume 4
c-2 consume 5
c-1 consume 6
p-1 product 7
p-3 product 8
p-2 product 9
c-3 consume 7
c-2 consume 8
c-1 consume 9
p-1 product 10
p-3 producer exit
p-2 producer exit
c-3 consume 10
c-2 consumer exit
c-1 consumer exit
p-1 producer exit
c-3 consumer exit
done.
从结果可以发现已经达到我们的目的了。
深入理解Condition的实现原理
上面的示例只是为了展示 Lock结合Condition可以实现的一种经典场景,在有了感性的认识之后,
我们将一步一步来观察Lock和Condition是如何协作完成这一任务的,这也是本篇的核心内容。
为了更好的理解和演示这一个过程,我们使用到的锁是使用公平策略模式的,我们会使用上面例子运作的流程。
我们会使用到3个生产线程,3个消费线程,分别表示 p1、p2、p3和c1、c2、c3。
Condition的内部实现是使用节点链来实现的,每个条件实例对应一个节点链,我们有notEmpty 和 notFull 两个条件实例,所以会有两个等待节点链。
一切准备就绪 ,开始我们的探索之旅。
1、线程c3执行,然后发现没有产品可以消费,执行 notEmpty.await,进入等待队列中等候。
2、线程c2和线程c1执行,然后发现没有产品可以消费,执行 notEmpty.await,进入等待队列中等候。
3、 线程 p1 启动,得到了锁,p1开始生产产品,这时候p3抢在p2之前,执行了lock操作,结果p2和p3都处于等待状态,入同步队列等待。
注意,本例中我们使用的是公平策略模式下的排它锁,由于p3抢先执行取锁操作,所以虽然p2和p3都被阻塞了,但是p3会优先被唤醒 。
4、这会,p1生产完毕,通知 not empty等待队列,可以唤醒一个等待线程节点了,然后释放了锁,释放锁会导致p3被唤醒,然后p1进入下一个循环,进入同步队列。
事情开始变得有趣了,p1执行一次生产后,执行了 notEmpty.signal,其效果就是把 not empty等待列表中的头节点,即c3节点移到同步等待列队中,重新参与抢占锁。
5、p3生产完了产品后,继续notEmpty.signal,同时释放锁,释放锁后会唤醒p2线程,然后p3在下一轮尝试获取锁的时候,再次入队。
6、接着,p2继续生产,生产后执行 notEmpty.signal,同时释放锁,释放锁后唤醒c3线程,然后p2在下一轮尝试取锁的时候,入列。
7、c3进行消费,你可以看到,现在 not empty等待列队中已经没有等待节点了,由于我们使用的是公平策略排它锁,
这就会导致同步队列中的节点一个接着一个执行,而目前同步队列中的节点排列为一生产,一消费,这不难可以知道,
接下来代码已经不会进入 wait条件了,所以一个一个轮流执行就是,比如c3,执行完了,继续notFull.signal();
然后释放锁,入队,这里要明白,notFull.signal();这句代码其实没有作用了,因为 not full等待队列中没有任何等待线程节点。
c3执行后,状态如下图所示:
8、后面的事情我想大家都可以想得出来是怎样一步一步交替执行的了。
总结
本篇基于一个实例来演示结合Lock和Condition如何实现生产-消费模式,而且只讨论一种可能执行的流程,是想更简单的表述AQS底层是如何实现的。
基于上面这个演示过程,针对其它的执行流程,其原来也是一样的。
Condition内部使用一个节点链来保存所有 wait状态的线程,当对应条件被signal的时候,就会把等待节点转移到同步队列中,
继续竞争锁。原理其实并不复杂,有兴趣的朋友可以翻阅源码。
java并发等待条件的实现原理(Condition)的更多相关文章
- Java 并发系列之二:java 并发机制的底层实现原理
1. 处理器实现原子操作 2. volatile /** 补充: 主要作用:内存可见性,是变量在多个线程中可见,修饰变量,解决一写多读的问题. 轻量级的synchronized,不会造成阻塞.性能比s ...
- Java 并发机制底层实现 —— volatile 原理、synchronize 锁优化机制
本书部分摘自<Java 并发编程的艺术> 概述 相信大家都很熟悉如何使用 Java 编写处理并发的代码,也知道 Java 代码在编译后变成 Class 字节码,字节码被类加载器加载到 JV ...
- 《Java并发编程的艺术》Java并发机制的底层实现原理(二)
Java并发机制的底层实现原理 1.volatile volatile相当于轻量级的synchronized,在并发编程中保证数据的可见性,使用 valotile 修饰的变量,其内存模型会增加一个 L ...
- 【java并发编程艺术学习】(三)第二章 java并发机制的底层实现原理 学习记录(一) volatile
章节介绍 这一章节主要学习java并发机制的底层实现原理.主要学习volatile.synchronized和原子操作的实现原理.Java中的大部分容器和框架都依赖于此. Java代码 ==经过编译= ...
- Java并发机制的底层实现原理之volatile应用,初学者误看!
volatile的介绍: Java代码在编译后会变成Java字节码,字节码被类加载器加载到JVM里,JVM执行字节码,最终需要转化为汇编指令在CPU上执行,Java中所使用的并发机制依赖于JVM的实现 ...
- Java并发基础类AbstractQueuedSynchronizer的实现原理简介
1.引子 Lock接口的主要实现类ReentrantLock 内部主要是利用一个Sync类型的成员变量sync来委托Lock锁接口的实现,而Sync继承于AbstractQueuedSynchroni ...
- Java并发编程笔记之ConcurrentHashMap原理探究
在多线程环境下,使用HashMap进行put操作时存在丢失数据的情况,为了避免这种bug的隐患,强烈建议使用ConcurrentHashMap代替HashMap. HashTable是一个线程安全的类 ...
- Java并发—–深入分析synchronized的实现原理
记得刚刚开始学习Java的时候,一遇到多线程情况就是synchronized,相对于当时的我们来说synchronized是这么的神奇而又强大,那个时候我们赋予它一个名字“同步”,也成为了我们解决多线 ...
- 并发艺术--java并发机制的底层实现原理
前言 Java代码在编译后会变成Java字节码,字节码被类加载器加载到JVM里,JVM执行字节码,最终需要转化为汇编指令在CPU上执行,Java中所使用的并发机制依赖于JVM的实现和CPU的指令. 一 ...
随机推荐
- git 操作规范
分支描述 长期存在 online 主分支,负责记录上线版本的迭代,该分支代码与线上代码是完全一致的. dev 开发分支,该分支记录相对稳定的版本,所有的feature分支都从该分支创建. 多套开发环境 ...
- jq demo 轮播图,图片可调用,向左,自动+鼠标点击切换
<!doctype html> <html> <head> <meta http-equiv="Content-Type" content ...
- java 集合之Map
Map的功能方法 方法put(Object key,Object value)添加一个"值"(想要得东西)和与"值"相关的"键"(key)( ...
- Android:layout属性大全
Android layout属性大全 第一类:属性值 true或者 false android:layout_centerHrizontal 水平居中android:layout_centerVert ...
- C# 连接池开发,多连接高效应用开发,多连接自动维护管理。
本文将使用一个Github开源的组件库技术来实现连接池的操作,应用于一些情况下的频繁的网络连接操作. github地址:https://github.com/dathlin/HslCommunicat ...
- linux c 输出信息到console
static void console_log(const char *format, ...) { static FILE *fpConsole; if (fpConsole == NULL) { ...
- IDEA 类图功能使用方法
1. Ctrl+Shift+Alt+U显示类图,(可以选中代码中类,再按快捷键,直接进入此类的类图) 2. 在类图中,选中某类右击显示Show Implementations,弹出子类的选择框. 按S ...
- ID3-C45-CART
区别:使用不同的属性选择度量. 信息增益偏向多值属性 信息增益率倾向产生不平衡的划分 基尼指数偏向多值属性,并且当类的数量很大时会有困难,还倾向于导致相等大小的分区和纯度 C4.5: 优点:产生的分类 ...
- 20155219 mybash的实现
第五周加分题--mybash的实现 题目要求 1.使用fork,exec,wait实现mybash 2.写出伪代码,产品代码和测试代码 3.发表知识理解,实现过程和问题解决的博客(包含代码托管链接) ...
- XXS level8
(1)查看PHP源代码 <?php ini_set("display_errors", 0); $str = strtolower($_GET["keyword&q ...