用阻塞队列实现一个生产者消费者模型?synchronized和lock有什么区别?
多线程当中的阻塞队列
主要实现类有
- ArrayBlockingQueue是一个基于数组结构的有界阻塞队列,此队列按FIFO原则对元素进行排序
- LinkedBlockingQueue是一个基于链表结构的阻塞队列,此队列按FIFO排序元素,吞吐量通常要高于ArrayBlockingQueue
- SynchronousQueue是一个不存储元素的阻塞队列,单个插入操作必须等到另一个线程调用移除操作,否则插 入操作一直处于阻塞状态
1. 阻塞队列概念
阻塞队列通俗来说,是一个队列,而一个阻塞队列再数据结构中所起的作用大致如下图
线程1往阻塞队列中添加元素,而线程2从阻塞队列中移除元素
当阻塞队列是空时,从队列中获取元素的操作会被阻塞
当阻塞队列是满时,从队列中添加元素的操作会被阻塞
- 试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他的线程向空的队列插入新的元素。
- 试图向已满的阻塞队列中添加新元素的线程同样会被阻塞,直到其他的线程从列中移除一个或者多个元素或者完全清空队列后使队列重新变得空闲起来并后续新增
很像生产者消费者模型!
2. 为什么要用阻塞队列?好处是什么?
在多线程领域:所谓阻塞,在某些情况下会挂起线程,一旦满足条件,被挂起的线程又会自动被唤醒
为什么需要BlockingQueue?
答:好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都给你一 手包办了
在concurrent包发布以前,在多线程环境下,我们每个程序员都必须自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们程序带来不小的复杂度
阻塞队列图示:
3. BlockingQueue的核心方法
方法类型 | 抛出异常 | 特殊值 | 阻塞 | 超时 |
---|---|---|---|---|
插入 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
移除 | remove() | poll() | take | poll(time,unit) |
检查 | element() | peek() | 不可用 | 不可用 |
可以看到,对于不同的方法类型,内部对应插入移除以及检查方法对应的api都不同,所以,想要对不同的对列操作时候,需要考虑是否需要抛出异常?线程是否需要阻塞等,这样对应不同的方法才能达到事半功倍的效果
参照源码
对上述方法类型做描述:
方法类型 | 描述 |
---|---|
抛出异常 | 当阻塞队列满时,再往队列中add会抛IllegalStateException: Queue full 当阻塞队列空时,在网队列里remove会抛 NoSuchElementException (这两个都是异常) |
特殊值 | 插入方法,成功true失败false 移除方法,成功返回出队列的元素,队列里没有就返回null |
阻塞 | 当阻塞队列满时,生产者线程继续往队列里put元素,队列会一直阻塞线程直到put数据或响应中断退出 当阻塞队列空时,消费者线程试图从队列take元素,队列会一直阻塞消费者线程直到队列可用 |
超时退出 | 当阻塞队列满时,队列会阻塞生产者线程一定时间 超过限时后生产者线程会退出 |
4. 种类分析
4.1 接口查看
我们知道,阻塞队列是BlockingQueue,我们打开Diagram图查看类之间的关系可得
Queue是继承了Queue的接口,同时Queue接口又继承了Collection接口,那么BlockingQueue作为接口,那么一定会有实现类,不同实现类使用不同数据结构实现,完成的操作也不相同,我们这里列出BlockingQueue的7个实现类,分别为:
- ArrayBlockingQueue:由数组结构组成的有界阻塞队列
- LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为 Integer.MAX_VALUE )阻塞队列
- PriorityBlockingQueue:支持优先级排序的无界阻塞队列
- DelayQueue:使用优先级队列实现的延迟无界阻塞队列
- SychronousQueue:不存储元素的阻塞队列,也即单个元素的队列
- LinkedTransferQueue:由链表结构组成的无界阻塞队列
- LinkedBlockingDeque:由双向链表结构组成的双向阻塞队列。
我们常用的实现类,在上面已经用重点符号表示了,ArrayBlockingQueue类似我们常用的ArrayList,底层数据结构是数组组成的队列,LinkedBlockingQueue就类似我们常用的LinkedList,底层数据结构是链表组成的队列,这里要注意LinkedBlockingDeque,那么Deque的接口继承关系如下所示:
对于我们常用的实现类,我们可以发现SychronousQueue我们不常用也不了解,什么是单个元素的队列?那么我们下面用代码实现一下:
4.2 SychronousQueue
概念:SynchronousQueue没有容量,与其他BlockingQueue不同,SychronousQueue是一个不存储元素的BlockingQueue,每一个put操作必须要等待一个take操作,否则不能继续添加元素,反之亦然
代码实例:
package com.yuxue.juc.queue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.SynchronousQueue;
public class SynchronousQueueDemo {
public static void main(String[] args) {
BlockingQueue blockingQueue = new SynchronousQueue<>();
//AAA线程主要用来对阻塞队列进行put操作
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + "\t" + "put 1");
blockingQueue.put(1);
System.out.println(Thread.currentThread().getName() + "\t" + "put 2");
blockingQueue.put(2);
System.out.println(Thread.currentThread().getName() + "\t" + "put 3");
blockingQueue.put(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "AAA").start();
//BBB线程主要从阻塞队列当中先操作自己的事务,休息5秒,之后拿值
new Thread(()->{
try {
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName()+"\t take:"+blockingQueue.take());
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName()+"\t take:"+blockingQueue.take());
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName()+"\t take:"+blockingQueue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
},"BBB").start();
}
}
代码结果为:
AAA put 1
//经过5秒
BBB take:1
AAA put 2
//经过5秒
BBB take:2
AAA put 3
//经过5秒
BBB take:3
5. 用在什么地方?
讲了这么多BlockingQueue的优点,那么阻塞队列一般用在哪里?
- 生产者消费者模式
5.1 传统版生产者消费者模式
package com.yuxue.juc.queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
//共享资源类
class Share1Data{
//内部共享变量
private volatile int num = 0;
//锁
private Lock lock = new ReentrantLock();
//condition进行通知
private Condition condition = lock.newCondition();
//内部对num进行自增的方法
public void increment() {
lock.lock();
try {
//循环判断
while (num != 0) {
condition.await();
}
//操作
num++;
System.out.println(Thread.currentThread().getName() + "\t" + num);
//通知
condition.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
//内部对num进行自减的方法
public void decrement() {
lock.lock();
try {
//循环判断
while (num == 0) {
condition.await();
}
//操作
num--;
System.out.println(Thread.currentThread().getName() + "\t" + num);
//通知
condition.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public class TraditionalProCons {
public static void main(String[] args) {
Share1Data dataShare = new Share1Data();
//10个线程进行num增加操作,对应生产者
for (int i = 0; i < 10; i++) {
new Thread(() -> {
dataShare.increment();
}, "Productor" + i).start();
}
//10个线程进行num减少操作,对应消费者
for (int i = 0; i < 10; i++) {
new Thread(() -> {
dataShare.decrement();
}, "Consumer" + i).start();
}
}
}
运行结果为:
Productor0 1
Consumer0 0
Productor1 1
Consumer1 0
Productor3 1
Consumer2 0
Productor5 1
Consumer3 0
Productor7 1
Consumer4 0
Productor8 1
Consumer5 0
Productor2 1
Consumer6 0
Productor9 1
Consumer7 0
Productor4 1
Consumer8 0
Productor6 1
Consumer9 0
可以看到对共享变量进行增加或者减少操作的时候需要进行通知,同时对内部变量进行volatile保证变量的可见性以及禁止指令重排,可以更好地对生产者消费者模型当中操作的保证
5.2 阻塞队列版生产者消费者模式
package com.yuxue.juc.queue;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
class ShareData {
//标志位,true表示默认开启生产者以及消费者
private volatile boolean flag = true;
//利用AtomicInteger类保证变量的原子性
private AtomicInteger atomicInteger = new AtomicInteger();
//建一个接口
BlockingQueue<String> blockingQueue = null;
//传递接口,主方法传递实际实现的类,可以实现代码复用
//编程时尽量传接口而不是传类
public ShareData(BlockingQueue<String> blockingQueue) {
this.blockingQueue = blockingQueue;
System.out.println(blockingQueue.getClass().getName());
}
//自己的生产者代码
public void myIncrement() throws InterruptedException {
String data = null;
boolean retValue = false;
//当开启时
while (flag) {
data = atomicInteger.getAndIncrement() + "";
//2s内添加成功返回true
retValue = blockingQueue.offer(data, 2, TimeUnit.SECONDS);
if (retValue) {
System.out.println(Thread.currentThread().getName() + "\t" + "插入" + data + "阻塞队列成功");
} else {
System.out.println(Thread.currentThread().getName() + "\t" + "插入" + data + "阻塞队列失败");
}
Thread.sleep(1000);
}
//当结束时,输出一下表示已经结束了
System.out.println(Thread.currentThread().getName() + "\t大老板叫停了, flag=false,生产结束");
}
//自己的消费者代码
public void myDecrement() throws InterruptedException {
String result = null;
//当开启时
while (flag) {
//2s内移除成功返回true
result = blockingQueue.poll(2, TimeUnit.SECONDS);
//内部判断,如果移除的是null,或者移除失败
if (null == result || result.equalsIgnoreCase("")) {
//让工作停止
flag = false;
System.out.println(Thread.currentThread().getName() + "\t超过 2s没有取到蛋糕,消费退出");
System.out.println();
//return的作用是不执行下面的成功语句
return;
}
System.out.println(Thread.currentThread().getName() + "\t消费队列" + result + "成功");
}
}
//停止生产者消费者模型
public void stop() throws Exception {
flag = false;
}
}
public class BlockingProCons {
public static void main(String[] args) {
//具体的实现类
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(5);
//共享资源
ShareData shareData = new ShareData(blockingQueue);
//生产线程
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t 生产线程启动");
try {
shareData.myIncrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"Productor").start();
//消费线程
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t 消费线程启动");
try {
shareData.myDecrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"Consumer").start();
try {
//主线程
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println();
System.out.println();
//主线程停止生产者消费者模型
System.out.println("5s后main叫停,线程结束");
try {
shareData.stop();
} catch (Exception e) {
e.printStackTrace();
}
}
}
结果:
//输出具体的实现类
java.util.concurrent.ArrayBlockingQueue
//生产者消费者模式
Productor 生产线程启动
Consumer 消费线程启动
Productor 插入0阻塞队列成功
Consumer 消费队列0成功
Productor 插入1阻塞队列成功
Consumer 消费队列1成功
Productor 插入2阻塞队列成功
Consumer 消费队列2成功
Productor 插入3阻塞队列成功
Consumer 消费队列3成功
Productor 插入4阻塞队列成功
Consumer 消费队列4成功
//停止操作
5s后main叫停,线程结束
Productor 老板叫停了, flag=false,生产结束
Consumer 超过 2s没有取到蛋糕,消费退出
可以看到上述并没有用到Lock以及synchronized,而仅仅用到了阻塞队列以及原子整型类,就可以实现生产者消费者模型,也就是不用程序员关心具体的加锁解锁过程,而是关心具体的业务逻辑
6. synchronized和lock有什么区别?用新的lock有什么好处?请举例
区别:
原始构成
synchronized是关键字属于jvm
其中jvm会将其字节码运行为monitorenter以及monitorexit
monitorenter,底层是通过monitor对象来完成,其实wait/notify等方法也依赖于monitor对象只有在同 步或方法中才能掉wait/notify等方法
monitorexit
Lock是具体类,是api层面的锁(java.util.concurrent.locks.Lock)
使用方法
- sychronized不需要用户取手动释放锁,当synchronized代码执行完后系统会自动让线程释放对锁的占用
- ReentrantLock则需要用户去手动释放锁若没有主动释放锁,就有可能导致出现死锁现象,需要lock()和 unlock()方法配合try/finally语句块来完成
等待是否可中断
- synchronized不可中断,除非抛出异常或者正常运行完成
- ReentrantLock可中断,设置超时方法
tryLock(long timeout, TimeUnit unit)
,或者lockInterruptibly()
放代码块中,调用interrupt()方法可中断
加锁是否公平
- synchronized是非公平锁
- ReentrantLock两者都可以,默认公平锁,构造方法可以传入boolean值,true为公平锁,false为非公平锁
锁绑定多个条件Condition
- synchronized没有
- ReentrantLock用来实现分组唤醒需要要唤醒的线程们,可以精确唤醒,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程
代码:
package com.yuxue.juc.queue; import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; /**
* synchronized和lock区别
* ===lock可绑定多个条件===
* 对线程之间按顺序调用,实现A>B>C三个线程启动,要求如下: * AA打印5次,BB打印10次,CC打印15次
* 紧接着
* AA打印5次,BB打印10次,CC打印15次
* 。。。。
* 来十轮
*/ class DataShare { private volatile int num = 0; private Lock lock = new ReentrantLock();
private Condition c1 = lock.newCondition();
private Condition c2 = lock.newCondition();
private Condition c3 = lock.newCondition(); public void print5() {
lock.lock();
try {
while (num != 0) {
c1.await();
}
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + i);
}
num = 1;
c2.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
} public void print10() {
lock.lock();
try {
while (num != 1) {
c2.await();
}
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + i);
}
num = 2;
c3.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
} public void print15() {
lock.lock();
try {
while (num != 2) {
c3.await();
}
for (int i = 0; i < 15; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + i);
}
num = 0;
c1.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
} public class SyncAndReentrantLockDemo {
public static void main(String[] args) {
DataShare dataShare = new DataShare(); for (int i = 0; i < 10; i++) {
new Thread(() -> {
dataShare.print5();
}, "AA").start(); new Thread(() -> {
dataShare.print10();
}, "BB").start(); new Thread(() -> {
dataShare.print15();
}, "CC").start();
}
}
}
我们可以清楚地看到Lock可以创建多个Condition,同时对不同的Condition调用await以及signal方法,可以对不同的线程进行操作,这就是Lock比synchronized更方便的原因,synchronized只能对所有线程进行notifyall()
方法,随机唤醒线程
注意:notifyall()
方法是Object类当中的方法!
用阻塞队列实现一个生产者消费者模型?synchronized和lock有什么区别?的更多相关文章
- Python 精进版SVIP版通过队列实现一个生产者消费者模型
import time from multiprocessing import Process,Queue,JoinableQueue #生产者 def producer(q): for i in r ...
- Python 再次改进版通过队列实现一个生产者消费者模型
import time from multiprocessing import Process,Queue #生产者 def producer(q): for i in range(10): time ...
- Python 通过队列实现一个生产者消费者模型
import time from multiprocessing import Process,Queue #生产者 def producer(q): for i in range(10): time ...
- Java阻塞队列(BlockingQueue)实现 生产者/消费者 示例
Java阻塞队列(BlockingQueue)实现 生产者/消费者 示例 本文由 TonySpark 翻译自 Javarevisited.转载请参见文章末尾的要求. Java.util.concurr ...
- Python学习笔记——进阶篇【第九周】———线程、进程、协程篇(队列Queue和生产者消费者模型)
Python之路,进程.线程.协程篇 本节内容 进程.与线程区别 cpu运行原理 python GIL全局解释器锁 线程 语法 join 线程锁之Lock\Rlock\信号量 将线程变为守护进程 Ev ...
- 进程同步控制(锁,信号量,事件), 进程通讯(队列和管道,生产者消费者模型) 数据共享(进程池和mutiprocess.Pool模块)
参考博客 https://www.cnblogs.com/xiao987334176/p/9025072.html#autoid-1-1-0 进程同步(multiprocess.Lock.Semaph ...
- Python守护进程、进程互斥锁、进程间通信ICP(Queue队列)、生产者消费者模型
知识点一:守护进程 守护进程:p1.daemon=True 守护进程其实就是一个“子进程“,守护=>伴随 守护进程会伴随主进程的代码运行完毕后而死掉 进程:当父进程需要将一个任务并发出去执行,需 ...
- JUC 并发编程--07 阻塞队列版本的 生产者消费者(不使用synchronized和 lock),也有一些疑惑,最终解惑
直接上代码: 前提是你已经 熟悉了原子类,volatile,和阻塞队列 public class JucPCdemo03 { /** * 阻塞队列的应用: 这里实现的生产者消费者,生产一个消费一个 * ...
- 快速实现一个生产者-消费者模型demo
package jesse.test1; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.Blo ...
随机推荐
- shell脚本就是由Shell命令组成的执行文件,将一些命令整合到一个文件中,进行处理业务逻辑,脚本不用编译即可运行。它通过解释器解释运行,所以速度相对来说比较慢。
shell脚本?在说什么是shell脚本之前,先说说什么是shell. shell是外壳的意思,就是操作系统的外壳.我们可以通过shell命令来操作和控制操作系统,比如Linux中的Shell命令就包 ...
- Linux如何查看文件的创建、修改时间?
Linux如何查看文件的创建.修改时间? 利用stat指令查看文件信息 三种时间的介绍 ATime --文件的最近访问时间 只要读取时间,ATime就会更新 MTime --文件的内容最近修改的时间 ...
- # useradd -u 700 -g users vbird2
[root@linux ~]# ls -l /homedrwxr-xr-x 3 vbird1 vbird1 4096 Aug 30 17:33 vbird1[root@linux ~]# grep v ...
- MyBatis 单表CURD操作(五)
MyBatis的CURD操作 添加CURD接口方法 package mapper; import entity.UserEntity; import org.apache.ibatis.annotat ...
- MyBatis 回顾 JDBC(一)
引言 学过 Java 的童鞋都知道,在 Java 中只有 JDBC 可以访问数据库,但是只要使用过 JDBC 的同学肯定也感受到 JDBC 访问数据库的繁琐, 需要编写大量的代码,经历一系列的步骤. ...
- Linux系统编程【5】——stty的学习
从文件的角度看设备 之前几篇文章介绍的编程是基于文件的.数据可以保存在文件中,也可以从文件中取出来做处理,再存回去.不仅如此,Linux操作系统还专门为这个东西建立了一套规则,就是前期介绍的" ...
- 一次线上事故,让我对MySql的时间戳存char(10)还是int(10)有了全新的认识
美好的周五 周五的早晨,一切都是那么美好. 然鹅,10点多的时候,运营小哥哥突然告诉我后台打不开了,我怀着一颗"有什么大不了的,估计又是(S)(B)不会连wifi"的心情,自信的打 ...
- 调试动态加载的js
用浏览器无法调试异步加载页面里包含的js文件.简单的说就是在调试工具里面看不到异步加载页面里包含的js文件 最近在一个新的web项目中开发功能.这个项目的管理界面有一个特点,框架是固定的,不会刷新 ...
- javascript获取日期,年月,日
<SCRIPT LANGUAGE="JavaScript"> var myDate = new Date(); myDate.getYear(); ...
- python_request 使用jsonpath取值结果,进行接口关联
一.jsonpath的安装 pip install jsonpath 二.使用举例 import jsonpath d1={"token":"hjshdsjhdsj ...