多线程下的阻塞队列特性及使用
Posted 若曦`
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程下的阻塞队列特性及使用相关的知识,希望对你有一定的参考价值。
1. 队列接口的常用子接口和实现类
2. BlockingQueue(阻塞队列)
四组API
方式 | 可抛出异常 | 不会抛出异常,有返回值= | 阻塞并等待 | 可超时等待 |
---|---|---|---|---|
添加 | add | offer(返回boolean) | put | offer(timenum,timeUnit) |
移除 | remove | poll(返回移除的元素) | take | poll(timenum,timeUnit) |
判断队列首 | element | peek | - | - |
(1) ArrayBlockingQueue
① 构造函数的源码解析及公平与非公平访问
ArrayBlockingQueue内部的阻塞队列是通过重入锁ReenterLock和Condition条件队列实现的,所以ArrayBlockingQueue中的元素存在公平访问与非公平访问的区别
ArrayBlockingQueue的重载构造函数,说明可以设置为公平锁或非公平锁
公平访问与非公平访问的说明
公平访问队列:被阻塞的线程按照阻塞的先后顺序访问队列,即先阻塞的线程先访问队列
而非公平队列:当队列可用时,阻塞的线程将进入争夺访问资源的竞争中,也就是说谁先抢到谁就执行,没有固定的先后顺序
非公平锁容易引起饥饿者问题,也就是有一个线程一直抢不到cpu资源而一直无法执行,这个问题在我重入锁那一章博客有详解
② 使用示例
public static void testBlockingQueue(){
BlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue(3);
blockingQueue.add(1);
blockingQueue.add(2);
blockingQueue.add(3);
// blockingQueue.add(4); //1. add()会抛出异常:java.lang.IllegalStateException: Queue full (队列已满)
boolean offer1 = blockingQueue.offer(4); //2. offer()不会抛出异常,但会返回一个boolean值
try {
// 3. offer()的重载可设置添加的超时时间,如果到时间还未添加进去,则返回false;
boolean offer2 = blockingQueue.offer(4,2, TimeUnit.SECONDS);
System.out.println(offer1); //false
System.out.println(offer2); //false
} catch (InterruptedException e) {
e.printStackTrace();
}
blockingQueue.remove();
blockingQueue.remove();
blockingQueue.remove();
// blockingQueue.remove(); //1. remove()会抛出异常,因为此时队列为空,不能再移除
Integer poll1 = blockingQueue.poll(); //2. poll()不会抛出异常,但会返回移除的元素,为空的话则返回null
try {
// 3. poll()的重载可设置添加的超时时间,如果到时间还未移除元素成功,则返回null;
Integer poll2 = blockingQueue.poll(2, TimeUnit.SECONDS);
System.out.println(poll1); //null
System.out.println(poll2); //null
} catch (InterruptedException e) {
e.printStackTrace();
}
}
③ 使用put()和take()的特性,解决生产者与消费者问题
因为put()方法在队列满的时候,会使当前线程等待,take()方法在队列空的时候,也会使当前线程等待,联合使用作用和普通的等待唤醒机制一样,同时比Condition的方式更简单
//创建一个汉堡队列,容量设为1
static BlockingQueue<Integer> hamburgerQueue = new ArrayBlockingQueue(1);
public static void bounded_buffer_Problem(){
//开启50个生产者线程
for (int i = 0; i < 50; i++) {
new Thread(()->{
try {
hamburgerQueue.put(1); //使用put()向队列中添加一个汉堡 当队列满的时候,会进行等待
System.out.println(Thread.currentThread().getName()+"生产了一个汉堡,还剩下"+hamburgerQueue.size()+"个");
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
},"生产者"+i).start();
}
//开启50个消费者线程
for (int i = 0; i < 50; i++) {
new Thread(()->{
try {
hamburgerQueue.take(); //使用take()向队列中添加一个汉堡 当队列为空的时候,为进行等待
System.out.println(Thread.currentThread().getName()+"消费了一个汉堡,还剩下"+hamburgerQueue.size()+"个");
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
},"消费者"+i).start();
}
}
(2) LinkedBlockingQueue
构造函数的源码分析
LinkedBlockingQueue使用了两把重入锁,获取并移除元素时(如poll、take)对应使用take锁,添加元素时使用put锁
默认容量为Integer.MAX_VALUE,可指定容量大小
使用方法
LinkedBlockingQueue使用方法和ArrayBlockingQueue一样
其构造方法不需要指定容量,默认为Integer.MAX_VALUE
但在使用LinkedBlockingQueue时建议手动设置容量大小,避免运行时队列过大造成机器负载或者内存爆满等情况
3. LinkedBlockingQueue和ArrayBlockingQueue的差别
(1) 队列的大小不同
ArrayBlockingQueue初始化时必须指定大小,LinkedBlockingQueue可以指定容量,也可以使用默认容量(Integer.MAX_VALUE)
LinkedBlockingQueue如果使用默认容量,在无界的情况下,如果添加比移除快,可能会造成内存溢出的问题
(2) 数据存储的容器不同
ArrayBlockingQueue采用的是数组作为容器,而LinkedBlockingQueue采用的则是链表
(3) 增加删除的操作不同
ArrayBlockingQueue采用的是数组作为容器,在增加和删除元素时,不会产生或销毁任何额外的对象实例,而LinkedBlockingQueue则会生成一个额外的Node对象,这在长时间大数据操作的情况下,可能会影响GC(垃圾回收器)的性能
(4) 使用的锁不同
ArrayBlockingQueue添加操作和移除操作采用的同一个ReenterLock锁,而LinkedBlockingQueue实现的队列使用的是两把锁
LinkedBlockingQueue添加采用的是putLock,移除使用的是takeLock,这使得在高并发的情况下生产者和消费者可以并行地操作队列中的数据,从而提高使用队列的并发性
以上是关于多线程下的阻塞队列特性及使用的主要内容,如果未能解决你的问题,请参考以下文章