多线程当中的阻塞队列

主要实现类有

  • ArrayBlockingQueue是一个基于数组结构的有界阻塞队列,此队列按FIFO原则对元素进行排序
  • LinkedBlockingQueue是一个基于链表结构的阻塞队列,此队列按FIFO排序元素,吞吐量通常要高于ArrayBlockingQueue
  • SynchronousQueue是一个不存储元素的阻塞队列,单个插入操作必须等到另一个线程调用移除操作,否则插 入操作一直处于阻塞状态

1. 阻塞队列概念

阻塞队列通俗来说,是一个队列,而一个阻塞队列再数据结构中所起的作用大致如下图

线程1往阻塞队列中添加元素,而线程2从阻塞队列中移除元素

当阻塞队列是空时,从队列中获取元素的操作会被阻塞

当阻塞队列是满时,从队列中添加元素的操作会被阻塞

  • 试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他的线程向空的队列插入新的元素。
  • 试图向已满的阻塞队列中添加新元素的线程同样会被阻塞,直到其他的线程从列中移除一个或者多个元素或者完全清空队列后使队列重新变得空闲起来并后续新增

很像生产者消费者模型!

2. 为什么要用阻塞队列?好处是什么?

  1. 在多线程领域:所谓阻塞,在某些情况下会挂起线程,一旦满足条件,被挂起的线程又会自动被唤醒

  2. 为什么需要BlockingQueue?

    答:好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都给你一 手包办了

  3. 在concurrent包发布以前,在多线程环境下,我们每个程序员都必须自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们程序带来不小的复杂度

  4. 阻塞队列图示:

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的优点,那么阻塞队列一般用在哪里?

  1. 生产者消费者模式

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有什么好处?请举例

区别:

  1. 原始构成

    • synchronized是关键字属于jvm

      其中jvm会将其字节码运行为monitorenter以及monitorexit

      monitorenter,底层是通过monitor对象来完成,其实wait/notify等方法也依赖于monitor对象只有在同 步或方法中才能掉wait/notify等方法

      monitorexit

    • Lock是具体类,是api层面的锁(java.util.concurrent.locks.Lock)

  2. 使用方法

    • sychronized不需要用户取手动释放锁,当synchronized代码执行完后系统会自动让线程释放对锁的占用
    • ReentrantLock则需要用户去手动释放锁若没有主动释放锁,就有可能导致出现死锁现象,需要lock()和 unlock()方法配合try/finally语句块来完成
  3. 等待是否可中断

    • synchronized不可中断,除非抛出异常或者正常运行完成
    • ReentrantLock可中断,设置超时方法tryLock(long timeout, TimeUnit unit),或者lockInterruptibly()放代码块中,调用interrupt()方法可中断
  4. 加锁是否公平

    • synchronized是非公平锁
    • ReentrantLock两者都可以,默认公平锁,构造方法可以传入boolean值,true为公平锁,false为非公平锁
  5. 锁绑定多个条件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有什么区别?的更多相关文章

  1. Python 精进版SVIP版通过队列实现一个生产者消费者模型

    import time from multiprocessing import Process,Queue,JoinableQueue #生产者 def producer(q): for i in r ...

  2. Python 再次改进版通过队列实现一个生产者消费者模型

    import time from multiprocessing import Process,Queue #生产者 def producer(q): for i in range(10): time ...

  3. Python 通过队列实现一个生产者消费者模型

    import time from multiprocessing import Process,Queue #生产者 def producer(q): for i in range(10): time ...

  4. Java阻塞队列(BlockingQueue)实现 生产者/消费者 示例

    Java阻塞队列(BlockingQueue)实现 生产者/消费者 示例 本文由 TonySpark 翻译自 Javarevisited.转载请参见文章末尾的要求. Java.util.concurr ...

  5. Python学习笔记——进阶篇【第九周】———线程、进程、协程篇(队列Queue和生产者消费者模型)

    Python之路,进程.线程.协程篇 本节内容 进程.与线程区别 cpu运行原理 python GIL全局解释器锁 线程 语法 join 线程锁之Lock\Rlock\信号量 将线程变为守护进程 Ev ...

  6. 进程同步控制(锁,信号量,事件), 进程通讯(队列和管道,生产者消费者模型) 数据共享(进程池和mutiprocess.Pool模块)

    参考博客 https://www.cnblogs.com/xiao987334176/p/9025072.html#autoid-1-1-0 进程同步(multiprocess.Lock.Semaph ...

  7. Python守护进程、进程互斥锁、进程间通信ICP(Queue队列)、生产者消费者模型

    知识点一:守护进程 守护进程:p1.daemon=True 守护进程其实就是一个“子进程“,守护=>伴随 守护进程会伴随主进程的代码运行完毕后而死掉 进程:当父进程需要将一个任务并发出去执行,需 ...

  8. JUC 并发编程--07 阻塞队列版本的 生产者消费者(不使用synchronized和 lock),也有一些疑惑,最终解惑

    直接上代码: 前提是你已经 熟悉了原子类,volatile,和阻塞队列 public class JucPCdemo03 { /** * 阻塞队列的应用: 这里实现的生产者消费者,生产一个消费一个 * ...

  9. 快速实现一个生产者-消费者模型demo

    package jesse.test1; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.Blo ...

随机推荐

  1. 一文带你搞懂 RPC 到底是个啥

    RPC(Remote Procedure Call),是一个大家既熟悉又陌生的词,只要涉及到通信,必然需要某种网络协议.我们很可能用过HTTP,那么RPC又和HTTP有什么区别呢?RPC还有什么特点, ...

  2. 从Lombok到JSR-269

    前言 Lombok的出现帮助开发人员在开发工程中消除了大部分冗余代码:繁琐的get.set方法甚至建造者模式. Lombok的实现方式是什么呢? 新建一个测试类使用Lombok的Getter和Sett ...

  3. LIN总线多从机与主机通信(控制+反馈)

    概念 首先要明确以下两点: LIN 总线通信方式为主从机通信,属于异步通信 "帧头 + 数据帧"才算一完整报文 设计方案 多从机与主机通信数据流(控制+反馈)如下: 一号从机: 第 ...

  4. CoSky-Mirror 就像一个镜子放在 Nacos、CoSky 中间,构建一个统一的服务发现平台

    CoSky 基于 Redis 的服务治理平台(服务注册/发现 & 配置中心) Consul + Sky = CoSky CoSky 是一个轻量级.低成本的服务注册.服务发现. 配置服务 SDK ...

  5. Docker学习(11) Dockerfile指令

    Dockerfile指令 指令格式 FROM MAINTAINER RUN EXPOSE CMD ENTRYPOINT ADD COPY VOLUME WORKDIR ENV USER ONBUILD ...

  6. Azure DevOps(二)利用Azure DevOps Pipeline 构建基础设施资源

    一,引言 上一篇文章记录了利用 Azure DevOps 跨云进行构建 Docker images,并且将构建好的 Docker Images 推送到 AWS 的 ECR 中.今天我们继续讲解 Azu ...

  7. AlexeyAB DarkNet YOLOv3框架解析与应用实践(二)

    AlexeyAB DarkNet YOLOv3框架解析与应用实践(二) 版本3有什么新功能? YOLOv3使用了一些技巧来改进训练和提高性能,包括:多尺度预测.更好的主干分类器等等.全部细节都在我们的 ...

  8. 图分析Rapids cuGraph

    图分析Rapids cuGraph 英伟达(Nvidia)建立的新的开源库可能是推进分析和使图形数据库更快的秘密要素. 在Nvidia GPU上进行并行处理. Nvidia很久以前就不再只是" ...

  9. https ssl(tls)为什么不直接用公钥加密数据?

    很多人都提到了非对称加密速度慢,但这只是一个原因,但不是主要原因,甚至是微不足道的原因. SSL协议到3.0后就已经到头了,取而代之的是TLS,相较于SSL的"安全套接字层"的命名 ...

  10. Git学习笔记(快速上手)

    Git学习 1. 基本使用 安装成功后在开始菜单中会有Git项,菜单下有3个程序:任意文件夹下右键也可以看到对应的程序! Git Bash:Unix与Linux风格的命令行,使用最多,推荐最多 Git ...