阻塞队列:当队列中没有元素时停止拿取,当队列中元素已经满时停止向队列中添加元素。使用场景可以联想到海底捞火锅,当餐厅位置已经满时,需要一个等候区来容纳进不到餐厅的客人,需要去阻塞而不是将客人赶走。
BlockQueue核心方法
方法类型 抛出异常 特殊值 阻塞 超时
插入 add offer put offer(e,time,unit)
移除 remove poll take poll
应用实例(生产者消费者)
原始生产者消费者代码如下
import java.lang.invoke.LambdaConversionException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class ShareData{
private int number = 0;
private Lock lock = new ReentrantLock();//里面不传递参数为false,为非公平锁
private Condition condition = lock.newCondition();
//生产者
public void inCreate() throws Exception{
lock.lock();
try{
//1. 判断(多线程时不要使用if判断,可能会导致虚假唤醒)
while (number!=0){
//当有产品时不生产
condition.await();
}
//2.干活
number++;
System.out.println(Thread.currentThread().getName()+"\t生产了"+number);
//3.通知唤醒
condition.signalAll();
}catch(Exception e){
e.printStackTrace();
}finally{
lock.unlock();
}
}
//消费者
public void takeCustomer() throws Exception{
lock.lock();
try{
//1. 判断(多线程时不要使用if判断,可能会导致虚假唤醒)
while (number == 0){
//当有产品时不消费
condition.await();
}
//2.干活
number--;
System.out.println(Thread.currentThread().getName()+"\t消费了"+number);
//3.通知唤醒
condition.signalAll();
}catch(Exception e){
e.printStackTrace();
}finally{
lock.unlock();
}
}
}
public class ProAndCusOld {
public static void main(String[] args) {
ShareData shareData = new ShareData();
new Thread(() ->{
for(int i = 1;i<=5;i++){
try{
shareData.inCreate();
}
catch (Exception e){
e.printStackTrace();
}
}
}).start();
new Thread(() ->{
for(int i = 1;i<=5;i++){
try{
shareData.takeCustomer();
}
catch (Exception e){
e.printStackTrace();
}
}
}).start();
}
}
可以看到使用lock时,需要手动去控制线程的唤醒,在并发环境下容易出现失误,下面是使用阻塞队列的方法
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
class MyResource{
private volatile Boolean flag = true; //使用volatile保证可见性
private AtomicInteger atomicInteger = new AtomicInteger();//在多线程条件下使用原子整型
BlockingDeque<String> blockingDeque = null; //阻塞队列
public MyResource(BlockingDeque<String> blockingDeque){ //构造注入传接口
this.blockingDeque = blockingDeque;
System.out.println(blockingDeque.getClass().getName());
}
//生产者
public void myProd() throws Exception{
String data = null;
Boolean returnVal;
while (flag){
data = atomicInteger.incrementAndGet()+"";//原子整型加一
returnVal = blockingDeque.offer(data,2l,TimeUnit.SECONDS);//插入队列
if(returnVal){
System.out.println(Thread.currentThread().getName()+"\t插入队列"+data+"成功");
}else{
System.out.println(Thread.currentThread().getName()+"\t插入队列"+data+"失败");
}
TimeUnit.SECONDS.sleep(2);
}
System.out.println(Thread.currentThread().getName()+"\t叫停");
}
//消费者
public void myCus() throws Exception{
String result = null;
while (flag){
result = blockingDeque.poll(2l,TimeUnit.SECONDS);//从队列中取出
if(null == result || result.equalsIgnoreCase("")){
flag = false;
System.out.println(Thread.currentThread().getName()+"\t"+"超过两秒没有取到");
return;
}
System.out.println(Thread.currentThread().getName()+"\t消费队列成功");
}
}
public void stop() throws Exception{
this.flag = false;
}
}
public class ProAndCusNew {
public static void main(String[] args) {
MyResource myResource = new MyResource(new ArrayBlockingQueue<String>(10));
new Thread(() ->{
System.out.println(Thread.currentThread().getName()+"\t生产线程启动");
try {
myResource.myProd();
}
catch (Exception e){
e.printStackTrace();
}
}).start();
new Thread(() ->{
System.out.println(Thread.currentThread().getName()+"\t消费线程启动");
try {
myResource.myCus();
}
catch (Exception e){
e.printStackTrace();
}
}).start();
try{
TimeUnit.SECONDS.sleep(5);
System.out.println("5s时间到不再执行");
myResource.stop();
}
catch (Exception e){
e.printStackTrace();
}
}
}
可以看到在使用阻塞队列之后,不用再去控制唤醒等这些细节,可以在保证能效的前提下同时兼顾安全。
阻塞队列的思想在消息中间键和线程池等地方都有使用,原理是类似的。