并发编程-阻塞队列

325 阅读6分钟

今天来看一下 JDK 中的阻塞队列,这个容器也很重要,因为我们做消息中间件必须用到。

什么是阻塞队列?就是给普通的队列增加阻塞操作。

分类

JAVA 里提供了 7 个阻塞队列,分别是:

  • ArrayBlockingQueue:一个由数据结构组成的有界阻塞队列
  • LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列
  • PriortyBlockingQueue:一个支持优先级排序的无界阻塞队列
  • DelayQueue:一个使用优先级队列实现的无界阻塞队列
  • SynchronousQueue:一个不存储元素的阻塞队列
  • LinkedTransferQueue:一个由链表结构组成的无界阻塞队列
  • LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列

ArrayBlockingQueue

ArrayBlockingQueue是用一个数组实现的有界阻塞队列,是一个FIFO队列。默认是非公平的(不按照线程阻塞的顺序插入或者弹出)。

LinkedBlockingQueue

LinkedBlockingQueue一个由链表结构组成的有界阻塞队列,此队列的默认和最大长度为Integer.MAX_VALUE,也是一个FIFO队列。

PriortyBlockingQueue

PriortyBlockingQueue是一个支持优先级排序的无界阻塞队列,默认按照元素自然顺序升序排列。也可以自定类实现compareTo()来指定排序规则。或者初始化时,指定构造参数Comparator来对元素进行排序。需要注意的是,如果两个元素优先级相同,不能保证它们的顺序。

DelayQueue

DelayQueue是一个支持延时获取元素的无界阻塞队列。队列使用PriortyBlockingQueue来实现。队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才可以提取元素。

介绍一下DelayQueue的用途:

  1. 缓存系统的设计

    可以用DelayQueue来保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦从DelayQueue中获取到数据,则说明这个缓存有效期到了。

  2. 定时任务调度

    使用DelayQueue保存当天将会执行的任务和时间,一旦从DelayQueue获取到任务就开始执行。比如TimerQueue就是基于DelayQueue实现的。

SynchronousQueue

SynchronousQueue是一个不存储元素的阻塞队列,每一个put操作必须等待一个take操作,否则不能继续添加元素。默认是非公平的。这个队列比较特殊,可以认为是一个传球手,负责把生产者处理的数据直接传递给消费者线程。队列本身不存储任何元素,非常适合传递性场景。SynchronousQueue的吞吐量高于ArrayBlockingQueueLinkedBlockingQueue。在cacheThreadPool中就是用了这种队列。

LinkedTransferQueue

LinkedTransferQueue是一个由链表结构组成的无界阻塞队列。相对于其他队列,它多了两个方法:transfer()和tryTransfer()。如果有消费者需要拿元素,transfer()方法可以把生产者传入的元素立即传给消费者;如果没有,则会把元素放在tail节点,等到该元素被消费了才返回,也就是说transfer()必须等到消费者消费了才返回,tryTransfer()顾名思义,尝试传输,不用等待如果传输失败直接返回 false。

LinkedBlockingDeque

LinkedBlockingDeque是一个由链表结构组成的双向阻塞队列,所谓双向就是可以队尾和队头插入和弹出元素。在初始化时可以设置容量,防止过度膨胀。双向阻塞队列可以用在工作窃取中。

原理

其实知道多线程几种阻塞机制,很容易就能实现阻塞的功能,比如wait-notifyreentrantLock.conditionLockSupport.park(this),其实阻塞队列也就是这几种模式实现的。

常用操作

生产者-消费者模式

使用BlockingQueue实现生产者-消费者模式进行解耦。

生产者,发出求救信号:

public class SOSProducer implements Runnable {

 private BlockingQueue<SOSData> queue;

 public SOSProducer(BlockingQueue<SOSData> queue) {
  this.queue = queue;
 }

 @Override
 public void run() {
  for (;;) {
   try {
    long waiting = ThreadLocalRandom.current().nextLong(5);
    TimeUnit.SECONDS.sleep(waiting);
    queue.put(new SOSData(System.currentTimeMillis()));
   } catch (InterruptedException e) {
    e.printStackTrace();
   }
  }
 }

}

消费者,接收求救信号并打印:

public class SOSConsumer implements Runnable {

 private BlockingQueue<SOSData> queue;

 public SOSConsumer(BlockingQueue<SOSData> queue) {
  this.queue = queue;
 }

 @Override
 public void run() {
  for (;;) {
   try {
    SOSData data = queue.take();
    System.out.println(Thread.currentThread().getName() + " 接收到求救信号:" + data);
   } catch (InterruptedException e) {
    e.printStackTrace();
   }
  }
 }

}

客户端:

private static final int PRODUCER_THREAD_NUM = 10;
private static final int CONSUMER_THREAD_NUM = 2;

public static void main(String[] args) {

 BlockingQueue<SOSData> queue = new LinkedBlockingQueue<SOSData>(100);

 ExecutorService producerPool = Executors.newFixedThreadPool(PRODUCER_THREAD_NUM);
 ExecutorService consumerPool = Executors.newFixedThreadPool(CONSUMER_THREAD_NUM);

 for (int i = 0; i < PRODUCER_THREAD_NUM; i++) {
  producerPool.execute(new SOSProducer(queue));
 }

 for (int i = 0; i < CONSUMER_THREAD_NUM; i++) {
  consumerPool.execute(new SOSConsumer(queue));
 }

}

console output:

pool-2-thread-2 接收到求救信号:SOSData [sosTime=1555426748063]
pool-2-thread-1 接收到求救信号:SOSData [sosTime=1555426748063]
pool-2-thread-2 接收到求救信号:SOSData [sosTime=1555426748064]
pool-2-thread-1 接收到求救信号:SOSData [sosTime=1555426748064]
pool-2-thread-2 接收到求救信号:SOSData [sosTime=1555426748063]
pool-2-thread-1 接收到求救信号:SOSData [sosTime=1555426749063]
pool-2-thread-2 接收到求救信号:SOSData [sosTime=1555426750063]
pool-2-thread-1 接收到求救信号:SOSData [sosTime=1555426750063]
pool-2-thread-2 接收到求救信号:SOSData [sosTime=1555426750063]

延迟订单

使用delayQueue实现,其实这种基于内存的不太可靠,这里演示下用法,方便写 demo 时使用:

/**
 * 延迟元素
 *
 */
public class DelayItem<Timplements Delayed {

 /**
  * 到期时间(执行时间)单位纳秒
  */
 private long executeTime;

 /**
  * 数据
  */
 private T data;

 // time是过期时长,也就是延迟多少毫秒 5*1000即为5秒
 public DelayItem(long delayTime, T data) {
  // 将传入的时长转为超时的时刻
  this.executeTime = TimeUnit.NANOSECONDS.convert(delayTime, TimeUnit.MILLISECONDS) + System.nanoTime();
  this.data = data;
 }

 // 按照剩余时间排序
 @Override
 public int compareTo(Delayed o) {
  long d = this.getDelay(TimeUnit.NANOSECONDS) - o.getDelay(TimeUnit.NANOSECONDS);
  return (d == 0) ? 0 : (d > 0 ? 1 : -1);
 }

 // 该方法返回还需要延时多少时间,单位为纳秒,所以设计的时候最好使用纳秒
 @Override
 public long getDelay(TimeUnit unit) {
  return unit.convert(executeTime - System.nanoTime(), TimeUnit.NANOSECONDS);
 }

 public long getExecuteTime() {
  return executeTime;
 }

 public T getData() {
  return data;
 }

}

总结

本文演示了下基本的阻塞队列,并没有对原理进行分析,其实实现原理并不复杂,就是基于显示锁的等待通知。

EOF