Concurrent型、CopyOnWrite型、Blocking型
常见问题
需要达到的水平:
- 哪些队列是有界的,哪些队列是无界的。
- 针对特定场景需求如何选择合适的队列实现。
- 从源码的角度常见的线程安全队列是如何实现的,并进行了哪些改进,以提高效率。
ConcurrentLinkedQueue 和 LinkedBlockingQueue区别:
- Concurrent类似于lock-free ,在常见的多线程场景中,一般可提供较高吞吐量。
- LinkedBlockingQueue 内部基于锁,并提供了BlockingQueue 的等待性方法
有事我们把并发包下的所有容器都习惯叫做并发容器,但严格说,类似ConcurrentLinkedQueue这种才是真正代表并发。
总结:
JUC 包下的容器中,大致分为三类:Concurrent、CopyOnWrite、Blocking这三种,同样都是线程安全容器,可以简单认为: - Concurrent 型没有类似于CopyOnWrite容器相对较重的修改开销。
- 凡是都有两面性,导致Concurrent 提供了较低的遍历一致性。你可以这样理解所谓的弱一致性:例如当利用迭代器遍历时,如果容器发生修改,迭代器仍然可以继续进行遍历。
- 与弱一致性相对应的是同步容器的另一种行为:“fast - fail” ,也就是检测到容器在遍历时发生了修改,则抛出ConcurrentModificationException,不再继续遍历。
- 弱一致性的另一个体现是,size等操作,未必100%准确。
- 与此同时读写的性能具有一定的不一致性。
部分线程安全队列实现图:
从基本的数据结构角度分析:
ConcurrentLinkedDeque 和 LinkedBlockingDeque,Deque的特点是双端队列,支持头和尾都进行插入删除操作。如
- 尾部插入时:addLast(e)、offerLast(e)
- 尾部删除时:removeLast()、pollLast()
从行为特征分析:
绝大部分的Queue都实现了BlockingQueue接口,Blocking意味着提供了特定的等待操作,获取时(take)等待元素进队,插入时(put)等待队列出现空位
BlockingQueue中哪些队列是有界的哪些队列是无界的
- ArrayBlockingQueue 是典型的有界队列,内部以final数组保护数据,数组的大小决定了队列的边界,所以我们在创建BlockingQueue时,都要指定容量如
public ArrayBlockingQueue(int capacity, boolean fair)
- LinkedBockingQueue 通常容易被误解为无边界,但是其实其行为和内部代码都是基于有界的逻辑实现的,只不过我们在创建时如果没有指定容量,那么其容量限制就自动的被设置成Integer.MAX_VALUE,成了无界队列。
- SynchronousQueue 是一个非常奇葩的队列实现,每一个删除操作都需要等待插入操作,反之一样,每个插入操作都要等待删除操作。且其内部容量是0
- PriorityBlockingQueue 是无边界的优先队列,但大小总要受系统资源影响。
- DelayedQueue 和LinkedTransferQueue 同样是无边界的队列。
注意:
对于无边界队列,put操作永远也不会出现BlockingQueue中的那种等待状态。
BlockingQueue底层实现
BlockingQueue基本是基于锁实现。下面通过LinkedBlockingQueue的源码来看
/*
*Lock held by take poll etc
*/
private final ReentrantLock takelock = new ReentrantLock();
/*
*wait queue for waiting takes
*/
private final Condition notEmpty = takeLock.newCondition();
/*
*Lock held by put offer etc
*/
private final ReentrantLock putlock = new ReentrantLock();
/*
*wait queue for waiting puts
*/
private final Condition notFull = putLock.newCondition();
ArrayBlockingQueue 的
notFull 和 notEmpty 都是同一个再入锁的条件变量,而LinkedBlocking则改进了锁操作的粒度,头尾操作使用不同的锁,所以在通用场景下,它的吞吐量相对要更好一些。
下面LinkedBlockingQueue的take方法实现和ArrayBlockingQueue 也是有不同的,由于其自身是链表,需要自己维护元素数量值。
public E take() throws InterruptedException {
final E x;
final int c;
final AutomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
while (count,get() == 0) {
notEmpty.await();
}
x = dequeue();
c = count.getAndDecrement();
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unLock();
}
if (c == capacity)
singalNotFull();
return x;
}
而类似ConcurrentLinkedQueue,则是基于CAS的无锁技术,不需要在每个操作时都使用锁,所以扩展性表现要更加优异。
相对比较另类的SynchronousQueue 在Java 6中发生了非常大的改变, 利用CAS替换掉了原本基于锁的逻辑,同步开销比较小,它也是Executors.newCacheThreadPool 的默认队列。
队列使用场景与典型用例
通过BlockingQueue来实现生产者消费者模式:
public class ProducerAndConsumer {
public static final String EXIT_MSG = "GOOD ... Bye!";
public static void main(String[] args) {
//使用较小的队列,以便更好的在输出中展示其影响。
BlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
Producer producer = new Producer(queue);
Consumer consumer = new Consumer(queue);
new Thread(producer).start();
new Thread(consumer).start();
}
static class Producer implements Runnable {
private BlockingQueue<String> queue;
public Producer(BlockingQueue<String> q) {
this.queue = q;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
try {
Thread.sleep(5L);
String msg = "Message" + i;
System.out.println("Produced new item: " + msg);
queue.put(msg);
}catch (InterruptedException e){
e.printStackTrace();
}
}
try {
System.out.println("Time to say goodBye!");
queue.put(EXIT_MSG);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
static class Consumer implements Runnable {
private BlockingQueue<String> queue;
public Consumer(BlockingQueue<String> q) {
this.queue = q;
}
@Override
public void run() {
try {
String msg;
while (!EXIT_MSG.equalsIgnoreCase((msg = queue.take()))) {
System.out.println("Consumed item:" + msg);
Thread.sleep(10L);
}
System.out.println("Got exit massage, bye!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
运行结果:
如果不使用Blocking队列,我们就需要自己实现轮询,条件判断(如检查poll返回值是否是null)等逻辑,如果没有特别的要求Blocking队列代码实现更加简单直观。
特定场景如何选择合适的队列实现
以LinkedBlockingqueue 和 ArrayBlockingQueue 和 SynchronousQueue为例。
- 考虑应用场景对边界应用的要求。ArrayBlockingQueue 有明确的容量限制,而LinkedBlockingQueue取决于我们是否在创建时指定。SynchronousQueue不能缓存任何数据。
- 从空间利用率的角度分析。ArrayBlockingQueue要比LinkedBlockingQueue紧凑,因为不需要创建节点。但初始分配阶段就需要一段连续的空间,所以初始内存需求更大。
- 通用场景中,LinkedBlockingQueue 的吞吐量比 ArrayBlockingQueue 更紧凑,因为它实现了更加细粒度的锁操作。
- ArrayBlockingQueue实现比较简单,性能更好预测,属于比较稳健的选手。
- 如果我们要实现的是两个线程间接力性(handoff)的场景,你可能会选CountDownLatch,但是SynchronousQueue也完美符合这种场景,而且线程间协调和数据统一起来,代码更加规范。
- 在队列元素较小的场景下,SynchronousQueue的性能表现,往往大大超过其他实现。