目录
  1. 1. 阻塞队列
    1. 1.1. 什么是阻塞队列
    2. 1.2. Java里的阻塞队列
      1. 1.2.1. ArrayBlockingQueue
      2. 1.2.2. LinkedBlockingQueue
      3. 1.2.3. PriorityBlockingQueue
      4. 1.2.4. DelayQueue
        1. 1.2.4.1. 如何实现Delayed接口
        2. 1.2.4.2. 如何实现延时阻塞
      5. 1.2.5. SynchronousQueue
      6. 1.2.6. LinkedTransferQueue
        1. 1.2.6.1. transfer方法
        2. 1.2.6.2. tryTransfer方法
      7. 1.2.7. LinkedBlockingDeque
    3. 1.3. 阻塞队列的实现原理
12-阻塞队列

阻塞队列

什么是阻塞队列

  阻塞队列(BlockingQueue)是一个支持两个附加操作的队列.这两个附加的操作支持阻塞的插入和移除方法

  1. 支持阻塞的插入方法:意思是当队列满时,队列会阻塞插入元素的线程,直到队列不满
  2. 支持阻塞的移除方法:意思是在队列为空时,获取元素的线程会等待队列变为非空

  阻塞队列常用于生产者和消费者的场景,生产者是向队列里添加元素的线程,消费者是从队列里取元素的线程.阻塞队列就是生产者用来存放元素、消费者用来获取元素的容器
在阻塞队列不可用时,这两个附加操作提供了4种处理方式,如下表:

方法/处理方式 抛出异常 返回特殊值 一直阻塞 超时退出
插入方法 add(e) offer(e) put(e) offer(e, time, unit)
移除方法 remove() poll() take() poll(time, unit)
检查方法 element() peek() 不可用 不可用
  • 抛出异常:当队列满时,如果再往队列里插入元素,会抛出IllegalStateException(“Queue full”)异常.当队列空时,从队列里获取元素会抛出NoSuchElementException异常
  • 返回特殊值:当往队列插入元素时,会返回元素是否插入成功,成功返回true.如果是移除方法,则是从队列里取出一个元素,如果没有则返回null
  • 一直阻塞:当阻塞队列满时,如果生产者线程往队列里put元素,队列会一直阻塞生产者线程,直到队列可用或者响应中断退出.当队列空时,如果消费者线程从队列里take元素,队列会阻塞住消费者线程,直到队列不为空
  • 超时退出:当阻塞队列满时,如果生产者线程往队列里插入元素,队列会阻塞生产者线程一段时间,如果超过了指定的时间,生产者线程就会退出

如果是无界阻塞队列,队列不可能会出现满的情况,所以使用put或offer方法永远不会被阻塞,而且使用offer方法时,该方法永远返回true

Java里的阻塞队列

JDK提供了7个阻塞队列,如下:

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

ArrayBlockingQueue

  ArrayBlockingQueue是一个用数组实现的有界阻塞队列.此队列按照先进先出(FIFO)的原则对元素进行排序
  默认情况下不保证线程公平的访问队列,所谓公平访问队列是指阻塞的线程,可以按照阻塞的先后顺序访问队列,即先阻塞线程先访问队列.非公平性是对先等待的线程是非公平的,当队列可用时,阻塞的线程都可以争夺访问队列的资格,有可能先阻塞的线程最后才访问队列.为了保证公平性,通常会降低吞吐量.我们可以使用以下代码创建一个公平的阻塞队列

1
ArrayBlockingQueue fairQueue = new ArrayBlockingQueue(1000,true);

访问者的公平性是使用可重入锁实现的,代码如下:

1
2
3
4
5
6
7
8
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}

LinkedBlockingQueue

  LinkedBlockingQueue是一个用链表实现的有界阻塞队列.此队列的默认和最大长度为Integer.MAX_VALUE.此队列按照先进先出的原则对元素进行排序

PriorityBlockingQueue

  PriorityBlockingQueue是一个支持优先级的无界阻塞队列.默认情况下元素采取自然顺序升序排列.也可以自定义类实现compareTo()方法来指定元素排序规则,或者初始化PriorityBlockingQueue时,指定构造参数Comparator来对元素进行排序.需要注意的是不能保证同优先级元素的顺序

DelayQueue

  DelayQueue是一个支持延时获取元素的无界阻塞队列.队列使用PriorityQueue来实现.队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素.只有在延迟期满时才能从队列中提取元素
DelayQueue非常有用,可以将DelayQueue运用在以下应用场景:

  • 缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素时,表示缓存有效期到了
  • 定时任务调度:使用DelayQueue保存当天将会执行的任务和执行时间,一旦从DelayQueue中获取到任务就开始执行,比如TimerQueue就是使用DelayQueue实现的

如何实现Delayed接口

  DelayQueue队列的元素必须实现Delayed接口.我们可以参考ScheduledThreadPoolExecutor里ScheduledFutureTask类的实现,一共有三步

  1. 在对象创建的时候,初始化基本数据.使用time记录当前对象延迟到什么时候可以使用,使用sequenceNumber来标识元素在队列中的先后顺序.代码如下:
    1
    2
    3
    4
    5
    6
    7
    private static final AtomicLong sequencer = new AtomicLong(0);
    ScheduledFutureTask(Runnable r, V result, long ns, long period) {
    super(r, result);
    this.time = ns;
    this.period = period;
    this.sequenceNumber = sequencer.getAndIncrement();
    }
  2. 实现getDelay方法,该方法返回当前元素还需要延时多长时间,单位是纳秒,代码如下:
    1
    2
    3
    public long getDelay(TimeUnit unit) {
    return unit.convert(time - now(), TimeUnit.NANOSECONDS);
    }
      通过构造函数可以看出延迟时间参数ns的单位是纳秒,自己设计的时候最好使用纳秒,因为实现getDelay()方法时可以指定任意单位,一旦以秒或分作为单位,而延时时间又精确不到纳秒就麻烦了.使用时请注意当time小于当前时间时,getDelay会返回负数
  3. 实现compareTo方法来指定元素的顺序.例如:让延时时间最长的放在队列的末尾.实现代码如下
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public int compareTo(Delayed other) {
    if (other == this) // compare zero ONLY if same object
    return 0;
    if (other instanceof ScheduledFutureTask) {
    ScheduledFutureTask<> x = (ScheduledFutureTask<>)other;
    long diff = time - x.time;

    if (diff < 0)
    return -1;
    else if (diff > 0)
    return 1;
    else if (sequenceNumber < x.sequenceNumber)
    return -1;
    else
    return 1;
    }
    long d = (getDelay(TimeUnit.NANOSECONDS) - other.getDelay(TimeUnit.NANOSECONDS));
    return (d == 0) 0 : ((d < 0) -1 : 1);
    }

    如何实现延时阻塞

      延时阻塞队列的实现很简单,当消费者从队列里获取元素时,如果元素没有达到延时时间,就阻塞当前线程
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    long delay = first.getDelay(TimeUnit.NANOSECONDS);
    if (delay <= 0)
    return q.poll();
    else if (leader != null)
    available.await();
    else {
    Thread thisThread = Thread.currentThread();
    leader = thisThread;
    try {
    available.awaitNanos(delay);
    } finally {
    if (leader == thisThread)
    leader = null;
    }
    }
      代码中的变量leader是一个等待获取队列头部元素的线程.如果leader不等于空,表示已经有线程在等待获取队列的头元素.所以,使用await()方法让当前线程等待信号.如果leader等于空,则把当前线程设置成leader,并使用awaitNanos()方法让当前线程等待接收信号或等待delay时间

    SynchronousQueue

      SynchronousQueue是一个不存储元素的阻塞队列.每一个put操作必须等待一个take操作,否则不能继续添加元素
      它支持公平访问队列.默认情况下线程采用非公平性策略访问队列.使用以下构造方法可以创建公平性访问的SynchronousQueue,如果设置为true,则等待的线程会采用先进先出的顺序访问队列
    1
    2
    3
    public SynchronousQueue(boolean fair) {
    transferer = fair ? new TransferQueue() : new TransferStack();
    }
      SynchronousQueue可以看成是一个传球手,负责把生产者线程处理的数据直接传递给消费者线程.队列本身并不存储任何元素,非常适合传递性场景.SynchronousQueue的吞吐量高于LinkedBlockingQueue和ArrayBlockingQueue

    LinkedTransferQueue

      LinkedTransferQueue是一个由链表结构组成的无界阻塞TransferQueue队列.相对于其他阻塞队列,LinkedTransferQueue多了tryTransfer和transfer方法

    transfer方法

      如果当前有消费者正在等待接收元素(消费者使用take()方法或带时间限制的poll()方法时),transfer方法可以把生产者传入的元素立刻transfer(传输)给消费者.如果没有消费者在等待接收元素,transfer方法会将元素存放在队列的tail节点,并等到该元素被消费者消费了才返回.transfer方法的关键代码如下
    1
    2
    Node pred = tryAppend(s, haveData);
    return awaitMatch(s, pred, e, (how == TIMED), nanos);
      第一行代码是试图把存放当前元素的s节点作为tail节点.第二行代码是让CPU自旋等待消费者消费元素.因为自旋会消耗CPU,所以自旋一定的次数后使用Thread.yield()方法来暂停当前正在执行的线程,并执行其他线程

    tryTransfer方法

      tryTransfer方法是用来试探生产者传入的元素是否能直接传给消费者.如果没有消费者等待接收元素,则返回false.和transfer方法的区别是tryTransfer方法无论消费者是否接收,方法立即返回,而transfer方法是必须等到消费者消费了才返回
      对于带有时间限制的tryTransfer(E e,long timeout,TimeUnit unit)方法,试图把生产者传入的元素直接传给消费者,但是如果没有消费者消费该元素则等待指定的时间再返回,如果超时还没消费元素,则返回false,如果在超时时间内消费了元素,则返回true

    LinkedBlockingDeque

      LinkedBlockingDeque是一个由链表结构组成的双向阻塞队列.所谓双向队列指的是可以从队列的两端插入和移出元素.双向队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争.相比其他的阻塞队列,LinkedBlockingDeque多了addFirst、addLast、offerFirst、offerLast、peekFirst和peekLast等方法,以First单词结尾的方法,表示插入、获取(peek)或移除双端队列的第一个元素.以Last单词结尾的方法,表示插入、获取或移除双端队列的最后一个元素.另外插入方法add等同于addLast,移除方法remove等效于removeFirst.但是take方法却等同于takeFirst,不知道是不是JDK的bug,使用时还是用带有First和Last后缀的方法更清楚
      在初始化LinkedBlockingDeque时可以设置容量防止其过度膨胀.另外双向阻塞队列可以运用在”工作窃取”模式中

    阻塞队列的实现原理

      如果队列是空的,消费者会一直等待,当生产者添加元素时,消费者是如何知道当前队列有元素的呢?如果让你来设计阻塞队列你会如何设计,如何让生产者和消费者进行高效率的通信呢?让我们先来看看JDK是如何实现的
      使用通知模式实现.所谓通知模式就是当生产者往满的队列里添加元素时会阻塞住生产者,当消费者消费了一个队列中的元素后,会通知生产者当前队列可用.通过查看JDK源码发现ArrayBlockingQueue使用了Condition来实现,代码如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    private final Condition notFull;
    private final Condition notEmpty;

    public ArrayBlockingQueue(int capacity, boolean fair) {
    // 省略其他代码
    notEmpty = lock.newCondition();
    notFull = lock.newCondition();
    }

    public void put(E e) throws InterruptedException {
    checkNotNull(e);
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
    while (count == items.length)
    notFull.await();
    insert(e);
    } finally {
    lock.unlock();
    }
    }

    public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
    while (count == 0)
    notEmpty.await();
    return extract();
    } finally {
    lock.unlock();
    }
    }

    private void insert(E x) {
    items[putIndex] = x;
    putIndex = inc(putIndex);
    ++count;
    notEmpty.signal();
    }
    当往队列里插入一个元素时,如果队列不可用,那么阻塞生产者主要通过LockSupport.park(this)来实现
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public final void await() throws InterruptedException {
    if (Thread.interrupted())
    throw new InterruptedException();
    Node node = addConditionWaiter();
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    while (!isOnSyncQueue(node)) {
    LockSupport.park(this);
    if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
    break;
    }
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
    interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // clean up if cancelled
    unlinkCancelledWaiters();
    if (interruptMode != 0)
    reportInterruptAfterWait(interruptMode);
    }
    继续进入源码,发现调用setBlocker先保存一下将要阻塞的线程,然后调用unsafe.park阻塞当前线程
    1
    2
    3
    4
    5
    6
    public static void park(Object blocker) {
    Thread t = Thread.currentThread();
    setBlocker(t, blocker);
    unsafe.park(false, 0L);
    setBlocker(t, null);
    }
    unsafe.park是个native方法,代码如下:
    1
    public native void park(boolean isAbsolute, long time);
    park这个方法会阻塞当前线程,只有以下4种情况中的一种发生时,该方法才会返回:
  • 与park对应的unpark执行或已经执行时.”已经执行”是指unpark先执行,然后再执行park的情况
  • 线程被中断时
  • 等待完time参数指定的毫秒数时
  • 异常现象发生时,这个异常现象没有任何原因
文章作者: Eric Liang
文章链接: https://ericql.github.io/2019/11/12/02-Java%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/01-%E5%BA%94%E7%94%A8%E7%AF%87/12-%E9%98%BB%E5%A1%9E%E9%98%9F%E5%88%97/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Eric Liang
打赏
  • 微信
  • 支付宝