什么是阻塞队列:
一句话总结:食堂排队打饭,就是一个阻塞队列
特点:
当队列是空
的,从队列中获取元素
的操作将会被阻塞
当队列是满
的,从队列中添加元素
的操作将会被阻塞
队列为空
的时候不能消费
,满
的时候不能添加
就是阻塞队列
举个例子,在蛋糕店里面买面包,如果面包卖完了,那么消费者就不能买了,如果装蛋糕的柜子已经放满了,那么做蛋糕的师傅就不会再生产蛋糕了
用处:
在某些情况下会挂起线程(阻塞), 一旦条件满足,被挂起的线程又会自动被唤醒
为什么需要,好处是什么:
不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,不需要手动控制 wait 和 notify,因为这一切阻塞队列都搞好了
BlockingQueue在java类中的架构
BlockingQueue接口是Queue接口的子接口,Queue接口是Collection接口的子接口,List接口也是Collection接口的子接口,所以Queue应该和List接口具有相似的功能
阻塞队列中常见的实现类
ArrayBlockingQueue: 由数组结构组成的有界阻塞队列
LinkedBlockingQueue:由链表结构组成的有界 (但大小默认值为 integer.MAX_VALUE ) 阻塞队列
PriorityBlockingQueue:支持优先级排序的无界阻塞队列
DelayQueue:使用优先级队列实现的延迟无界阻塞队列
SynchronousQueue:不存储元素的阻塞队列,也既单个元素的队列
LinkedTransferQueue:由链表组成的无界阻塞队列
LinkedBlockingDeque
:由链表组成的双向阻塞队列
BlockingQueue阻塞队列中的核心方法及案例
核心方法总结:检查的时候都是从队列的头部开始检查元素的
,返回的都是队列中首个元素对象
抛出异常 | 当阻塞队列满时,再往队列里add插入元素会抛出ILLegalStateException:Queye full,返回boolean类型 当阻塞队列空时,再往队列里remove移出元素会抛出NoSuchElementException,返回取出的对象 |
---|---|
特殊值 | 插入方法,成功返回ture 失败返回false ,不会等待也不会抛出异常,队列满了就不能插入移出方法,成功 返回出队列的元素从队列的首位开始 ,不会等待也不会抛出异常,队列里面没有就返回null |
一直阻塞 | 当阻塞队列满 时,生产者线程继续往队列里put元素 ,队列会一直阻塞生产线程 直到 put数据 or 响应中断退出 ,没有返回值当阻塞队列 空 时,消费者线程试图从队列里take元素 ,队列会一直阻塞消费者线程直到队列可用 |
超时退出 | 当阻塞队列满 时,队列会阻塞生产者 线程一定时间,超过限制的时间后,生产者线程退出 当阻塞队列 空 时,队列会阻塞消费者 线程一段时间,超过限制后,消费者线程返回null |
例1:
创建一个界限长度为3的队列,如果
public class ArrayBlockingQueueDemo {
public static void main(String[] args) {
//创建一个界限为3的阻塞队列
ArrayBlockingQueue<String> arrayBlockingQueue = new ArrayBlockingQueue<>(3);
//往blockingqueue中添加3个元素
arrayBlockingQueue.add("a");
arrayBlockingQueue.add("b");
arrayBlockingQueue.add("c");
//添加第4个元素时,会出现 队列已满异常,类似于ArrayList中的下标越界异常
arrayBlockingQueue.add("d");
}
}
public class ArrayBlockingQueueDemo {
public static void main(String[] args) {
//创建一个界限为3的阻塞队列
BlockingQueue<String> arrayBlockingQueue = new ArrayBlockingQueue<>(3);
//往blockingqueue中添加3个元素,返回boolean类型
arrayBlockingQueue.add("a");
arrayBlockingQueue.add("b");
arrayBlockingQueue.add("c");
//检查队列中的队首元素是谁,打印结果为 a
System.out.println(arrayBlockingQueue.element());
//如果不指定清除的对象,那么清除的就是最先加入到队列中的元素
// arrayBlockingQueue.remove();
//如果指定清除的对象,那么清除的就是指定的对象
arrayBlockingQueue.remove("a");
//继续添加
arrayBlockingQueue.add("d");
}
}
常用方法:
add(), element(), remove()
例2
public class BlockingQueueDemo {
public static void main(String[] args) {
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
System.out.println(blockingQueue.offer("a"));
System.out.println(blockingQueue.offer("b"));
System.out.println(blockingQueue.offer("c"));
//检查队首的元素
System.out.println(blockingQueue.peek());
//清除队列中的元素, 从队首开始, 并返回被清除的元素
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
//取出元素,如果取出失败,则返回null,而不是抛出异常
System.out.println(blockingQueue.poll());
}
}
例3
public class BlockingQueueDemo2 {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
blockingQueue.put("a");
blockingQueue.put("b");
blockingQueue.put("c");
// System.out.println("==============================");
//添加第4个元素的时候,如果阻塞队列最多只能装3个元素的话,那么就会一直堵在这里,直到队列有位置让我把元素放进去
// blockingQueue.put("d");
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());
//取元素的时候 也是这样,如果队列中有元素让我取,我就把元素取出来,如果没有元素让我取,我就等 直到有元素让我取
System.out.println(blockingQueue.take());
System.out.println("==============================");
}
}
可以看到,程序还没有结束
例4
public class BlockingQueueDemo3 {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
System.out.println(blockingQueue.offer("a", 2L, TimeUnit.SECONDS));
System.out.println(blockingQueue.offer("b", 2L, TimeUnit.SECONDS));
System.out.println(blockingQueue.offer("c", 2L, TimeUnit.SECONDS));
//往队列中添加元素,如果添加了2s钟都没有添加进去,那么就不插入了
// System.out.println(blockingQueue.offer("d", 2L, TimeUnit.SECONDS));
System.out.println(blockingQueue.poll(2L, TimeUnit.SECONDS));
System.out.println(blockingQueue.poll(2L, TimeUnit.SECONDS));
System.out.println(blockingQueue.poll(2L, TimeUnit.SECONDS));
//从队列中取出元素,如果添加了2s钟都没有取出,那么就返回一个 null
System.out.println(blockingQueue.poll(2L, TimeUnit.SECONDS));
}
}
执行结果:
使用阻塞队列来验证生产者消费者模式
同时也可以验证 SynchronousQueue 是不存储元素的阻塞队列,也既单个元素的队列
/**
* 使用阻塞队列来验证生产者消费者模式
* 同时也可以验证 SynchronousQueue 是不存储元素的阻塞队列,也既单个元素的队列
*
* 思路:使用阻塞队列的put,take 方法 SynchronousQueue 每次只能往阻塞队列中添加一条记录
* put一个元素的时候,如果想要再次put,生产者线程只能进入阻塞状态
* take 一个元素的时候,如果想要再次take,和上面一样
*
* 开启2个线程,一个线程用来生产,一个线程用来消费
* 1.生产者线程往阻塞队列中添加一个元素 , 想再次添加,自己进入阻塞状态,
* 2.此时消费者线程开始执行,执行完成后,生产者线程马上就可以继续添加了
*
*/
public class UseBlockingQueueWithThreadCommunicate {
public static void main(String[] args) {
BlockingQueue<String> blockingQueue = new SynchronousQueue<>();
new Thread(() ->{
try {
System.out.println("生产者添加 a 元素");
blockingQueue.put("a");
System.out.println("生产者添加 b 元素");
blockingQueue.put("b");
System.out.println("生产者添加 c 元素");
blockingQueue.put("c");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "aa").start();
new Thread(() ->{
try {
TimeUnit.SECONDS.sleep(3);
System.out.println("消费者得到:"+blockingQueue.take());
TimeUnit.SECONDS.sleep(3);
System.out.println("消费者得到:"+blockingQueue.take());
TimeUnit.SECONDS.sleep(3);
System.out.println("消费者得到:"+blockingQueue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "bb").start();
}
}
对比使用Lock/阻塞队列生产者消费者模式的区别
使用Lock的方式
/**
* 需求:
* 一个面包房中的面包, 师傅生产一个, 消费者消费一个, 循环5次
*
* 知识点1. 线程进入await状态后, 线程会释放锁资源
* 知识点2. 创建线程的时候要搞清楚 到底是2个线程,各操作5次共享资源; 还是10个线程, 个操作1次共享资源
* 此例中 是2个线程各操作5次共享资源
* 知识点3. decrease(), increase()方法中执行判断的时候 需要使用 while 而不是 if 目的是防止虚假唤醒
*
*/
public class LockDemo {
public static void main(String[] args) {
CommonResource commonResource = new CommonResource();
//
// for (int i = 0; i < 5; i++) {
// new Thread(() -> commonResource.increase(), String.valueOf(i)).start();
// commonResource.increase();
// }
// for (int i = 0; i < 5; i++) {
// new Thread(() -> commonResource.decrease(), String.valueOf(i)).start();
// }
//##########################################################################################
new Thread(() -> {
for (int i = 0; i < 5; i++) {
commonResource.increase();
}
}, "AA").start();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
commonResource.decrease();
}
}, "BB").start();
}
}
class CommonResource {
private Integer num = 0;
private final ReentrantLock reentrantLock = new ReentrantLock();
private final Condition condition = reentrantLock.newCondition();
public void increase() {
reentrantLock.lock();
try {
while (num != 0) {
System.out.println("num为" + num + " 生产者线程阻塞");
condition.await();
}
num++;
System.out.println("添加1个后 现在的值为" + num);
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
}
public void decrease() {
reentrantLock.lock();
try {
while (num == 0) {
System.out.println("num为0 消费者线程被阻塞");
condition.await();
}
num--;
System.out.println("减少1个后 现在的值为" + num);
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
}
}
使用阻塞队列的方式,结合volatile/cas/atomicInteger/blockingqueue/线程交互
/**
* 使用阻塞队列完成 生产和消费蛋糕, 由<外部>控制生产和消费是否结束
*
* 知识点1. 为什么构造方法要传入BlockingQueue接口 而不是传入接口的某个实现类 -- 便于方法的扩展
* 知识点2. 为什么 FLAG 需要用到 volatile --- 保证可见性,FLAG被修改了之后 所有线程可见, 然后关闭生产和消费
* 知识点3. num 在本例中的作用不大, 只是把对象放入到阻塞队列中
*/
public class BlockingQueueDemo {
public static void main(String[] args) {
CommonResource3 commonResource3 = new CommonResource3(new ArrayBlockingQueue<>(2));
new Thread(() -> {
System.out.println("开始生产");
commonResource3.add();
}, "prod").start();
new Thread(() -> {
System.out.println("开始消费");
commonResource3.des();
}, "consumer").start();
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 关闭生产和消费
commonResource3.finish();
}
}
class CommonResource3 {
private volatile Boolean FLAG = true;
private AtomicInteger num = new AtomicInteger(0);
private BlockingQueue<String> blockingQueue;
public CommonResource3(BlockingQueue<String> blockingQueue) {
this.blockingQueue = blockingQueue;
//判断传入的阻塞队列是哪一种类型
System.out.println(blockingQueue.getClass().getName());
}
public void add() {
String val;
boolean offer;
while (FLAG) {
try {
TimeUnit.SECONDS.sleep(1);
val = String.valueOf(num.incrementAndGet());
offer = blockingQueue.offer(val, 2L, TimeUnit.SECONDS);
if (offer) {
System.out.println(Thread.currentThread().getName() + "线程 添加蛋糕, 当前蛋糕编号:" + val);
} else {
System.out.println(Thread.currentThread().getName() + "生产失败");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void des() {
String poll;
while (FLAG) {
try {
TimeUnit.SECONDS.sleep(1);
poll = blockingQueue.poll(2L, TimeUnit.SECONDS);
if (Objects.nonNull(poll)) {
System.out.println(Thread.currentThread().getName() + " 消费蛋糕" + poll);
} else {
FLAG = false;
System.out.println("关闭生产消费操作");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 外部关闭生产和消费
*/
public void finish() {
FLAG = false;
}
}
区别:
- 使用Lock的方式需要手动加锁,控制生产和消费
- 使用阻塞队列的方式不用,直接生产/消费,然后由外部控制是否继续执行即可