并发编程-10阻塞队列BlockingQueue

一 认识阻塞队列

1.1 Queue接口

jdk提供了一个顶层的队列接口,主要有一下方法:

public interface Queue extends Collection {

boolean add(E e);

boolean offer(E e);

E remove();

E poll();

E element();

E peek();

}

● add(E e):添加一个元素,添加成功返回true,如果队列满了,就会抛出异常。

● offer(E e):添加一个元素,添加成功返回true, 如果队列满了,返回false。

● remove(): 返回并删除队首元素,队列为空则抛出异常。

● poll(): 返回并删除队首元素,队列为空则返回null。

● element():返回队首元素,但不移除,队列为空则抛出异常。

● peek():获取队首元素,但不移除,队列为空则返回null

这是基于jdk的Queue接口,又提供了带阻塞功能的阻塞队列BlockingQueue接口。

1.2 BlockingQueue接口

BlockingQueue 继承了Queue 接口,是队列的一种。Queue和BlockingQueue 都是在 Java 5 中加入的。阻塞队列(BlockingQueue)是一个在队列基础上又支持了两个附加操作的队列,用于解耦。两个附加操作:

● 支持阻塞的插入方法put: 队列满时,队列会阻塞插入元素的线程,直到队列不满。

● 支持阻塞的移除方法take: 队列空时,获取元素的线程会等待队列变为非空

void put(E e) throws InterruptedException;

boolean offer(E e, long timeout, TimeUnit unit)throws InterruptedException;

E take() throws InterruptedException;

E poll(long timeout, TimeUnit unit)

throws InterruptedException;

BlockingQueue和JDK集合包中的Queue接口兼容,同时在其基础上增加了阻塞功能。

入队:

(1)offer(E e):如果队列没满,返回true,如果队列已满,返回false(不阻塞)。

(2)offer(E e, long timeout, TimeUnit unit):可以设置阻塞时间,如果队列已满,则进行阻塞。超过阻塞时间,则返回false。

(3)put(E e):队列没满的时候是正常的插入,如果队列已满,则阻塞,直至队列空出位置。

出队:

(1)poll():如果有数据,出队,如果没有数据,返回null(不阻塞)。

(2)poll(long timeout, TimeUnit unit):可以设置阻塞时间,如果没有数据,则阻塞,超过阻塞时间,则返回null。

(3)take():队列里有数据会正常取出数据并删除,但是如果队列里无数据,则阻塞,直到队列里有数据。

1.3 BlockingQueue使用

add(E e)方法:

add(E e)方法是往队列里添加一个元素,如果队列满了,就会抛出异常来提示队列已满

代码演示:

@Slf4j

public class BlockingQueueDemo1 {

public static void main(String[] args) {
    
    log.info("测试add方法====超过最大容量");
    addTest();

}


private static void addTest() {
    BlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(2);
    log.info("是否成功:{}",blockingQueue.add(1));
    log.info("是否成功:{}",blockingQueue.add(2));
    log.info("是否成功:{}",blockingQueue.add(3));
}

}

运行结果:

remove()方法:

remove()方法的作用是删除元素并返回队列的头节点,如果删除的队列是空的, remove()方法就会抛出异常。

代码演示:

@Slf4j

public class BlockingQueueDemo1 {

public static void main(String[] args) {

    log.info("测试remove方法====");
    removeTest();


}
    private static void removeTest() {
    ArrayBlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(2);
    blockingQueue.add(1);
    blockingQueue.add(2);
    log.info("删除元素:{}",blockingQueue.remove());
    log.info("删除元素:{}",blockingQueue.remove());
    log.info("删除元素:{}",blockingQueue.remove());
}

}

运行结果:

offer(E e)方法:

offer(E e)方法用来插入一个元素。如果添加成功会返回true,而如果队列已经满了,返回false

代码演示:

@Slf4j

public class BlockingQueueDemo1 {

public static void main(String[] args) {

    log.info("测试offer方法====");
    offerTest();


}

    private static void offerTest(){
    ArrayBlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(2);
    log.info("是否成功:{}",blockingQueue.offer(1));
    log.info("是否成功:{}",blockingQueue.offer(2));
    log.info("是否成功:{}",blockingQueue.offer(3));
}

}

运行结果:

poll()方法:

poll()方法作用也是移除并返回队列的头节点。 如果队列为空,返回null

代码演示:

@Slf4j

public class BlockingQueueDemo1 {

public static void main(String[] args) {

    log.info("测试poll方法====");
    pollTest();


}

    private static void pollTest() {
    ArrayBlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(3);
    blockingQueue.offer(1);
    blockingQueue.offer(2);
    blockingQueue.offer(3);
    log.info("删除元素:{}",blockingQueue.poll());
    log.info("删除元素:{}",blockingQueue.poll());
    log.info("删除元素:{}",blockingQueue.poll());
    log.info("删除元素:{}",blockingQueue.poll());
}

}

运行结果:

put(E e)方法:

put(E e)方法的作用是插入元素。如果队列已满就无法继续插入,阻塞插入线程,直至队列空出位置

代码演示:

@Slf4j

public class BlockingQueueDemo1 {

public static void main(String[] args) {

    log.info("测试put方法====");
    putTest();


}

    private static void putTest(){
    BlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(2);
    try {
        blockingQueue.put(1);
        blockingQueue.put(2);
        blockingQueue.put(3);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

}

运行结果:

当队列已满的时候,线程一直在阻塞。

take()方法:

take()方法的作用是获取并移除队列的头结点。如果执队列里无数据,则阻塞,直到队列里有数据

代码演示:

@Slf4j

public class BlockingQueueDemo1 {

public static void main(String[] args) {

    log.info("测试take方法====");
    takeTest();


}

    private static void takeTest(){
    BlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(2);
    try {
        blockingQueue.take();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

}

运行结果:

队列的元素是空的,所以会一直阻塞。

1.4 BlockingQueue特性

阻塞特性:

阻塞队列区别于其他类型的队列的最主要的特点就是"阻塞"这两个字,所以下面重点介绍阻塞功能:阻塞功能使得生产者和消费者两端的能力得以平衡,当有任何一端速度过快时,阻塞队列便会把过快的速度给降下来。实现阻塞最重要的两个方法是 take()方法和put(E e)方法。

take()方法:

take()方法的功能是获取并移除队列的头结点,通常在队列里有数据的时候是可以正常移除的。可是一旦执行take()方法的时候,队列里无数据,则阻塞,直到队列里有数据。一旦队列里有数据了,就会立刻解除阻塞状态,并且取到数据。过程如图所示:

put(E e)方法:

put(E e)方法插入元素时,如果队列没有满,那就和普通的插入一样是正常的插入,但是如果队列已满,那么就无法继续插入,则阻塞,直到队列里有了空闲空间。如果后续队列有了空闲空间,比如消费者消费了一个元素,那么此时队列就会解除阻塞状态,并把需要添加的数据添加到队列中。过程如图所示:

容量:

阻塞队列还有一个非常重要的属性,那就是容量的大小,分为有界和无界两种。无界队列意味着里面可以容纳非常多的元素,例如 LinkedBlockingQueue 的上限是 Integer.MAX_VALUE,是非常大的一个数,可以近似认为是无限容量,因为我们几乎无法把这个容量装满。但是有的阻塞队列是有界的,例如 ArrayBlockingQueue 如果容量满了,也不会扩容,所以一旦满了就无法再往里放数据了。

1.5 应用场景

BlockingQueue 是线程安全的,我们在很多场景下都可以利用线程安全的队列来优雅地解决我们业务自身的线程安全问题。比如说,使用生产者/消费者模式的时候,我们生产者只需要往队列里添加元素,而消费者只需要从队列里取出它们就可以了,如图所示:

因为阻塞队列是线程安全的,所以生产者和消费者都可以是多线程的,不会发生线程安全问题。生产者/消费者直接使用线程安全的队列就可以,而不需要自己去考虑更多的线程安全问 题。这也就意味着,考虑锁等线程安全问题的重任从"你"转移到了"队列"上,降低了我们开发的难度和工作量。

同时,队列它还能起到一个隔离的作用。比如说我们开发一个银行转账的程序,那么生产者线程不需要关心具体的转账逻辑,只需要把转账任务,如账户和金额等信息放到队列中就可以, 而不需要去关心银行这个类如何实现具体的转账业务。而作为银行这个类来讲,它会去从队列里取出来将要执行的具体的任务,再去通过自己的各种方法来完成本次转账。这样就实现了具体任务与执行任务类之间的解耦,任务被放在了阻塞队列中,而负责放任务的线程是无法直接访问我们银行具体实现转账操作的对象的,实现了隔离,提高了安全性。

二 常见阻塞队列和原理

2.1 常见的阻塞队列

BlockingQueue 接口的实现类都被放在了juc包中,它们的区别主要体现在存储结构上或对元素操作上的不同,但是对于take与put操作的原理,却是类似的。

2.2 ArrayBlockingQueue

2.2.1 ArrayBlockingQueue底层结构

public class ArrayBlockingQueue extends AbstractQueue

implements BlockingQueue, java.io.Serializable {

//元素数组
final Object[] items;

//下一个待取元素的索引
int takeIndex;

//下一个待放元素的索引
int putIndex;

//队列中元素数量
int count;

//锁
final ReentrantLock lock;

//队列为空的条件变量对象
private final Condition notEmpty;

//队列为满的条件变量对象
private final Condition notFull;

}

2.2.2 ArrayBlockingQueue构造方法

public ArrayBlockingQueue(int capacity) {

this(capacity, false);

}

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();

}

public ArrayBlockingQueue(int capacity, boolean fair,

Collection<? extends E> c) {

this(capacity, fair);

    final ReentrantLock lock = this.lock;
    lock.lock(); // Lock only for visibility, not mutual exclusion
    try {
        int i = 0;
        try {
            for (E e : c) {
                checkNotNull(e);
                items[i++] = e;
            }
        } catch (ArrayIndexOutOfBoundsException ex) {
            throw new IllegalArgumentException();
        }
        count = i;
        putIndex = (i == capacity) ? 0 : i;
    } finally {
        lock.unlock();
    }
}

2.2.3 put()方法原理

public void put(E e) throws InterruptedException {

//检查非空

checkNotNull(e);

//拿到锁对象

final ReentrantLock lock = this.lock;

//上锁

lock.lockInterruptibly();

try {

//如果队列达到了容量阈值

while (count == items.length)

//阻塞生产者

notFull.await();

//如果没有达到容量阈值,则入队

enqueue(e);

} finally {

//解锁

lock.unlock();

}

}

private void enqueue(E x) {

//拿到元素数组

final Object[] items = this.items;

//根据下一个放元素数组索引放下当前元素

items[putIndex] = x;

//如果刚好达到队列阈值,则重置下一个放元素数组索引,也就是形成环形索引

if (++putIndex == items.length)

putIndex = 0;

//没有达到就把数组长度+1

count++;

//把阻塞的消费者队列转为同步队列,然后通过unlock()方法唤醒。

notEmpty.signal();

}

大概逻辑:

1.入队的时候先加一把锁

2.判断是否达到队列容量,如果达到,阻塞生产者线程

3.如果没有达到队列容量,进行入队操作

4.根据下一个放元素数组索引放下当前元素

5.判断放入元素后的数组长度是否达到容量,如果达到则重置索引

6.元素数量+1

7.把阻塞的消费者队列转为同步队列,然后通过unlock()方法唤醒。

2.2.4 take()方法原理

public E take() throws InterruptedException {

//获取锁对象

final ReentrantLock lock = this.lock;

//加锁

lock.lockInterruptibly();

try {

//如果数组元素为空

while (count == 0)

//阻塞消费者线程

notEmpty.await();

//出队

return dequeue();

} finally {

lock.unlock();

}

}

private E dequeue() {

//拿到元素数组

final Object[] items = this.items;

@SuppressWarnings("unchecked")

//获取要拿取的元素

E x = (E) items[takeIndex];

//数组元素置空

items[takeIndex] = null;

//如果下次拿取元素的索引是最后一个

if (++takeIndex == items.length)

//置为空,重新从头获取

takeIndex = 0;

//元素数量减1

count--;

if (itrs != null)

itrs.elementDequeued();

//把阻塞的生产者队列转为同步队列,然后通过unlock()方法唤醒。

notFull.signal();

return x;

}

1.入队的时候先加一把锁

2.判断队列是否为空,如果为空,阻塞消费者线程

3.如果不为空,进行出队操作

4.根据下一个取元素数组索引取出当前元素

5.判断取出元素后的下一个要取元素的索引是否是队列的最后一个位置,如果是,把索引置为0,这样就形成环形数组来存取数据

6.元素数量-1

7.把阻塞的生产者队列转为同步队列,然后通过unlock()方法唤醒。

所以对于ArrayBlockQueue存取数据看下图:

2.3 LinkedBlockingQueue

LinkedBlockingQueue是一个基于链表实现的阻塞队列,默认情况下,该阻塞队列的大小为Integer.MAX_VALUE,由于这个数值特别大,所以 LinkedBlockingQueue 也被称作无界队 列,代表它几乎没有界限,队列可以随着元素的添加而动态增长,但是如果没有剩余内存, 则队列将抛出OOM错误。所以为了避免队列过大造成机器负载或者内存爆满的情况出现,我们在使用的时候建议手动传一个队列的大小。

LinkedBlockingQueue内部由单链表实现,只能从head取元素,从tail添加元素。 LinkedBlockingQueue采用两把锁的锁分离技术实现入队出队互不阻塞,添加元素和获取元素 都有独立的锁,也就是说LinkedBlockingQueue是读写分离的,读写操作可以并行执行。

2.3.1 LinkedBlockingQueue底层结构

public class LinkedBlockingQueue extends AbstractQueue

implements BlockingQueue, java.io.Serializable {

    //内部链表节点类
    static class Node<E> {
    E item;

    /**
     * One of:
     * - the real successor Node
     * - this Node, meaning the successor is head.next
     * - null, meaning there is no successor (this is the last node)
     */
    Node<E> next;

    Node(E x) { item = x; }
}


//容量
private final int capacity;

//元素数量
private final AtomicInteger count = new AtomicInteger();


//头节点
transient Node<E> head;

//尾节点
private transient Node<E> last;

//出队锁
private final ReentrantLock takeLock = new ReentrantLock();

//队列为空的条件变量对象
private final Condition notEmpty = takeLock.newCondition();


//入队锁
private final ReentrantLock putLock = new ReentrantLock();

//队列满的时候条件变量对象
private final Condition notFull = putLock.newCondition();

}

2.3.2 LinkedBlockingQueue构造方法

public LinkedBlockingQueue() {

this(Integer.MAX_VALUE);

}

public LinkedBlockingQueue(int capacity) {

if (capacity <= 0) throw new IllegalArgumentException();

this.capacity = capacity;

last = head = new Node(null);

}

public LinkedBlockingQueue(Collection<? extends E> c) {

this(Integer.MAX_VALUE);

final ReentrantLock putLock = this.putLock;

putLock.lock(); // Never contended, but necessary for visibility

try {

int n = 0;

for (E e : c) {

if (e == null)

throw new NullPointerException();

if (n == capacity)

throw new IllegalStateException("Queue full");

enqueue(new Node(e));

++n;

}

count.set(n);

} finally {

putLock.unlock();

}

}

2.3.3 put()方法原理

public void put(E e) throws InterruptedException {

if (e == null) throw new NullPointerException();

    int c = -1;
    Node<E> node = new Node<E>(e);
    final ReentrantLock putLock = this.putLock;
    final AtomicInteger count = this.count;
    putLock.lockInterruptibly();
    try {
        //元素数量达到容量阈值
        while (count.get() == capacity) {
            //阻塞生产者线程
            notFull.await();
        }
        //入队
        enqueue(node);
        //拿到元素数量原值
        c = count.getAndIncrement();
        //如果还有空闲位置存放元素
        if (c + 1 < capacity)
            //将条件队列上的生产者线程转换到同步队列中,这里是生产者唤醒生产者,而不用消费者那边必须消费了才去唤醒阻塞的生产者线 程,最后通过unlock方法唤醒
            notFull.signal();
    } finally {
        putLock.unlock();
    }
    //原先数组没元素,但是线程已经存放了一个元素,唤醒阻塞的消费者线程
    if (c == 0)
        signalNotEmpty();
}

enqueue:

private void enqueue(Node node) {

// assert putLock.isHeldByCurrentThread();

// assert last.next == null;

last = last.next = node;

}

把上一个尾节点的next指向node,再把node指向last。

signalNotEmpty:

private void signalNotEmpty() {

final ReentrantLock takeLock = this.takeLock;

takeLock.lock();

try {

notEmpty.signal();

} finally {

takeLock.unlock();

}

}

唤醒条件队列中的消费者线程

大体逻辑:

当我们向阻塞队列put一个元素的时候

1.先放一个put锁

2.判断元素数量是否达到队列容量阈值,如果达到,阻塞生产者线程

3.如果没有达到容量阈值,进入入队操作,把当前元素放到队列队尾

4.如果还没有达到容量阈值,将条件队列上的生产者线程转换到同步队列中,这里是生产者唤醒生产者,而不用消费者那边必须消费了才去唤醒阻塞的生产者线程,最后通过unlock方法唤醒

5.如果队列之前是空的,放入了第一个元素,唤醒阻塞的消费者线程

2.3.4 take()方法原理

public E take() throws InterruptedException {

E x;

int c = -1;

//元素数量

final AtomicInteger 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();

}

//如果这次取元素之前,队列是满的,唤醒生产者线程put元素

if (c == capacity)

signalNotFull();

return x;

}

dequeue:

private E dequeue() {

// assert takeLock.isHeldByCurrentThread();

// assert head.item == null;

Node h = head;

Node first = h.next;

h.next = h; // help GC

head = first;

E x = first.item;

first.item = null;

return x;

}

把head下一个节点置为新的节点,并返回元素。

signalNotFull:

private void signalNotFull() {

final ReentrantLock putLock = this.putLock;

putLock.lock();

try {

notFull.signal();

} finally {

putLock.unlock();

}

}

唤醒生产者线程。

当我们向阻塞队列take一个元素的时候

1.先放一个take锁

2.判断队列容量是否为空,如果为空,阻塞消费者线程

3.如果队列不为空,进入出队操作,把当前元素返回,并把元素所在节点置为新的头节点

4.如果队列中还有元素,将条件队列上的消费者线程转换到同步队列中,这里是消费者唤醒消费者,而不用生产者那边必须生产了才去唤醒阻塞的消费者线程,最后通过unlock方法唤醒。

5.如果队列之前是满的,取入了第一个元素,唤醒阻塞的生产者线程。

2.3.5 LinkedBlockingQueue与ArrayBlockingQueue对比

LinkedBlockingQueue是一个阻塞队列,内部由两个ReentrantLock来实现出入队的线程安全,由各自的Condition对象的await和signal来实现等待和唤醒功能。它和ArrayBlockingQueue的不同点在于:

● 队列大小有所不同,ArrayBlockingQueue是有界的初始化必须指定大小,而 LinkedBlockingQueue可以是有界的也可以是无界的(Integer.MAX_VALUE),对于后者而言,当添加速度大于移除速度时,在无界的情况下,可能会造成内存溢出等问题。

● 数据存储容器不同,ArrayBlockingQueue采用的是数组作为数据存储容器,而LinkedBlockingQueue采用的则是以Node节点作为连接对象的链表。

● 由于ArrayBlockingQueue采用的是数组的存储容器,因此在插入或删除元素时不会产生或销毁任何额外的对象实例,而LinkedBlockingQueue则会生成一个额外的Node对象。这可能在长时间内需要高效并发地处理大批量数据的时,对于GC可能存在较大影响。

● 两者的实现队列添加或移除的锁不一样,ArrayBlockingQueue实现的队列中的锁是没有分离的,即添加操作和移除操作采用的同一个ReenterLock锁,而 LinkedBlockingQueue实现的队列中的锁是分离的,其添加采用的是putLock,移除采用的则是takeLock,这样能大大提高队列的吞吐量,也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。

2.4 SynchronousQueue

SynchronousQueue是一个没有数据缓冲的BlockingQueue,生产者线程对其的插入操作put必须等待消费者的移除操作take。

如图所示,SynchronousQueue最大的不同之处在于,它的容量为0,所以没有一个地方来暂存元素,导致每次取数据都要先阻塞,直到有数据被放入;同理,每次放数据的时候也会阻塞,直到有消费者来取。 需要注意的是,SynchronousQueue的容量不是1而是 0,因为 SynchronousQueue 不需要去持有元素,它所做的就是直接传递(direct handoff)。由于每当需要传递的时候, SynchronousQueue 会把元素直接从生产者传给消费者,在此期间并不需要做存储,所以如果运用得当,它的效率是很高的。

2.4.1 SynchronousQueue应用场景

SynchronousQueue非常适合传递性场景做交换工作,生产者的线程和消费者的线程同步传递某些信息、事件或者任务。

SynchronousQueue的一个使用场景是在线程池里,如果我们不确定来自生产者请求数量,但是这些请求需要很快的处理掉,那么配合SynchronousQueue为每个生产者请求分配一个消费线程是处理效率最高的办法。Executors.newCachedThreadPool()就使用了 SynchronousQueue,这个线程池根据需要(新任务到来时)创建新的线程,如果有空闲线程则会重复使用,线程空闲了60秒后会被回收。

2.4.2 SynchronousQueue的使用

当我们只有生产者,没有消费者消费消息的时候:

@Slf4j

public class SynchronousQueueDemo01 {

private static   SynchronousQueue<Integer> synchronousQueue = new SynchronousQueue();

public static void main(String[] args) throws InterruptedException {

      new Thread(()->{
          try {
              put(1);
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
      },"生产者1").start();
    Thread.sleep(1000);
    new Thread(()->{
        try {
            put(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    },"生产者2").start();

}


private static void put(Integer e) throws InterruptedException {
    log.info("存放数据{}",e);
    synchronousQueue.put(e);
}

private static void take() throws InterruptedException {
    log.info("取出数据:{}",synchronousQueue.take());

}

}

运行结果:

线程不终止,一直阻塞

我们添加两个消费者:

@Slf4j

public class SynchronousQueueDemo01 {

private static   SynchronousQueue<Integer> synchronousQueue = new SynchronousQueue();

public static void main(String[] args) throws InterruptedException {

      new Thread(()->{
          try {
              put(1);
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
      },"生产者1").start();
    Thread.sleep(1000);
    new Thread(()->{
        try {
            put(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    },"生产者2").start();

    new Thread(()->{
        try {
            take();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    },"消费者1").start();
    Thread.sleep(1000);
    new Thread(()->{
        try {
            take();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    },"消费者2").start();


}

private static void put(Integer e) throws InterruptedException {
    log.info("存放数据{}",e);
    synchronousQueue.put(e);
}

private static void take() throws InterruptedException {
    log.info("取出数据:{}",synchronousQueue.take());

}

}

运行结果:

消费者消费后,线程关闭。

2.4.4 SynchronousQueue数据结构

SynchronousQueue是支持公平和非公平方式的,它是基于它的内部数据结构来实现的

公平模式:TransferQueue

非公平模式:TransferStack

由于SynchronousQueue本身容量是0,是不存储数据的,所以在生产者和消费者之间是需要直接进行数据传输的,SynchronousQueue抽象了一个内部类:

abstract static class Transferer {

/**

* Performs a put or take.

*

* @param e if non-null, the item to be handed to a consumer;

* if null, requests that transfer return an item

* offered by producer.

* @param timed if this operation should timeout

* @param nanos the timeout, in nanoseconds

* @return if non-null, the item provided or received; if null,

* the operation failed due to timeout or interrupt --

* the caller can distinguish which of these occurred

* by checking Thread.interrupted.

*/

abstract E transfer(E e, boolean timed, long nanos);

}

无论是生产者调用put方法还是消费者调用take方法,他们都会调用transfer方法

public void put(E e) throws InterruptedException {

if (e == null) throw new NullPointerException();

if (transferer.transfer(e, false, 0) == null) {

Thread.interrupted();

throw new InterruptedException();

}

}

    public E take() throws InterruptedException {
    E e = transferer.transfer(null, false, 0);
    if (e != null)
        return e;
    Thread.interrupted();
    throw new InterruptedException();
}

2.4.5 公平模式实现原理

static final class TransferQueue extends Transferer {

static final class QNode {

//下一个节点

volatile QNode next; // next node in queue

//节点数据

volatile Object item; // CAS'ed to or from null

//当前线程

volatile Thread waiter; // to control park/unpark

//是否是put线程

final boolean isData;

}

}

transfer方法解析;

E transfer(E e, boolean timed, long nanos) {

//存放put的元素的节点

QNode s = null;

//ture代表put操作 false代表take操作

boolean isData = (e != null);

for (;😉 {

//尾节点

QNode t = tail;

//头节点

QNode h = head;

//不重要分支,可以省略

if (t == null || h == null)

continue;

//首尾相等,说明是空链表或者当前操作和链表尾部节点操作一样,满足的话进行入队和阻塞操作

if (h == t || t.isData == isData) { // empty or same-mode

//拿到尾巴节点的next

                //创建新的节点
                if (s == null)
                    s = new QNode(e, isData);
                //把尾节点的next指向新节点
                if (!t.casNext(null, s))        // failed to link in
                    continue;
              //新节点设置为新的tail节点
                advanceTail(t, s);              // swing tail and wait
                //这是个重要方法,看下边的方法讲解
                Object x = awaitFulfill(s, e, timed, nanos);
                if (x == s) {                   // wait was cancelled
                    clean(t, s);
                    return null;
                }

                if (!s.isOffList()) {           // not already unlinked
                    advanceHead(t, s);          // unlink if head
                    if (x != null)              // and forget fields
                        s.item = s;
                    s.waiter = null;
                }
                return (x != null) ? (E)x : e;
                //存在链表,且当前线程和队尾节点不是一个操作,准备唤醒队列第一个线程
            } else { 
                //拿到头节点的下一个节点
                QNode m = h.next;  
                if (t != tail || m == null || h != head)
                    continue;                   // inconsistent read
                Object x = m.item;
                if (isData == (x != null) ||    // m already fulfilled
                    x == m ||                   // m cancelled
                    //给下一个节点传递数据
                    !m.casItem(x, e)) {         // lost CAS
                    advanceHead(h, m);          // dequeue and retry
                    continue;
                }
              //唤醒的线程所在节点重新置为头节点
                advanceHead(h, m); 
                //唤醒线程
                LockSupport.unpark(m.waiter);
                return (x != null) ? (E)x : e;
            }
        }
    }

Object awaitFulfill(QNode s, E e, boolean timed, long nanos) {

/* Same idea as TransferStack.awaitFulfill */

final long deadline = timed ? System.nanoTime() + nanos : 0L;

Thread w = Thread.currentThread();

int spins = ((head.next == s) ?

(timed ? maxTimedSpins : maxUntimedSpins) : 0);

for (;😉 {

if (w.isInterrupted())

s.tryCancel(e);

Object x = s.item;

//如果当前节点的元素不为空,直接返回,否则自旋后进行阻塞

if (x != e)

return x;

if (timed) {

nanos = deadline - System.nanoTime();

if (nanos <= 0L) {

s.tryCancel(e);

continue;

}

}

if (spins > 0)

--spins;

else if (s.waiter == null)

s.waiter = w;

else if (!timed)

LockSupport.park(this);

else if (nanos > spinForTimeoutThreshold)

LockSupport.parkNanos(this, nanos);

}

}

这里对源码逻辑大概梳理一下:以消费者线程先启动为例:

1.当一个消费者线程调用take方法:

2.进入transfer()方法,如果链表没有节点或者链表尾部的节点也是一个消费者线程,那么就执行入队操作

创建一个节点,把当前尾巴节点的next指针指向新的节点,再把新节点置为尾巴节点,调用awaitFulfill()方法,后边来到消费者线程同样的方 法以队尾的方法进行入队。

3.设置自旋次数,只有头节点后边的第一个节点才可以自旋,判断当前节点是否有元素item,有的话说明生产者把数据传递过来了,直接返回数据

4.没有的话进行自旋重试,超过自旋阈值,调用park方法阻塞。

当消费者进入阻塞的时候,生产者线程来进行put操作

同样会调用transfer()方法:

5.此时判断链表有节点或者链表尾部节点是一个消费者线程

6.取出头节点的下一个节点,把元素通过cas操作赋值给头节点下边的第一个节点

7.设置头节点的下一个节点作为新的节点m.casItem(x, e))

if (isData == (x != null) || // m already fulfilled

x == m || // m cancelled

!m.casItem(x, e)) { // lost CAS

advanceHead(h, m); // dequeue and retry

continue;

}

8.唤醒消费者线程

当消费者线程被唤醒的时候,x不为空,直接返回x

Object x = s.item;

if (x != e)

return x;

接着就是出队操作:把waiter置为空

if (!s.isOffList()) { // not already unlinked

advanceHead(t, s); // unlink if head

if (x != null) // and forget fields

s.item = s;

s.waiter = null;

}

return (x != null) ? (E)x : e;

2.4.6 非公平模式实现原理

内部结构:

static final class TransferStack extends Transferer {

    /** Node represents an unfulfilled consumer */
    static final int REQUEST    = 0;
    /** Node represents an unfulfilled producer */
    static final int DATA       = 1;
    /** Node is fulfilling another unfulfilled DATA or REQUEST */
    static final int FULFILLING = 2;

    static final class SNode {
        //栈结构下个节点
        volatile SNode next;        // next node in stack
        //匹配到的节点
        volatile SNode match;       // the node matched to this
        //阻塞的线程
        volatile Thread waiter;     // to control park/unpark
        //传输的数据
        Object item;                // data; or null for REQUESTs
        int mode;
    }
}

transfer方法

E transfer(E e, boolean timed, long nanos) {

/*

* Basic algorithm is to loop trying one of three actions:

*

* 1. If apparently empty or already containing nodes of same

* mode, try to push node on stack and wait for a match,

* returning it, or null if cancelled.

*

* 2. If apparently containing node of complementary mode,

* try to push a fulfilling node on to stack, match

* with corresponding waiting node, pop both from

* stack, and return matched item. The matching or

* unlinking might not actually be necessary because of

* other threads performing action 3:

*

* 3. If top of stack already holds another fulfilling node,

* help it out by doing its match and/or pop

* operations, and then continue. The code for helping

* is essentially the same as for fulfilling, except

* that it doesn't return the item.

*/

        SNode s = null; // constructed/reused as needed
        //代表当前线程类型 1是生产者线程 0是消费者线程
        int mode = (e == null) ? REQUEST : DATA;

        for (;;) {
            SNode h = head;
            //如果还没形成栈结构或者当前操作的线程和栈尾的线程是相同操作
            if (h == null || h.mode == mode) {  // empty or same-mode
                if (timed && nanos <= 0) {      // can't wait
                    if (h != null && h.isCancelled())
                        casHead(h, h.next);     // pop cancelled node
                    else
                        return null;
                    //构建新节点并放在栈顶
                } else if (casHead(h, s = snode(s, e, h, mode))) {
                    //这个方法是进行阻塞的方法,可以往下看
                    SNode m = awaitFulfill(s, timed, nanos);
                    if (m == s) {               // wait was cancelled
                        clean(s);
                        return null;
                    }
                    if ((h = head) != null && h.next == s)
                        casHead(h, s.next);     // help s's fulfiller
                    return (E) ((mode == REQUEST) ? m.item : s.item);
                }
               /
            } else if (!isFulfilling(h.mode)) { // try to fulfill
                if (h.isCancelled())            // already cancelled
                    casHead(h, h.next);         // pop and retry
                //创建新的节点并置为头节点,这也是栈的体现
                else if (casHead(h, s=snode(s, e, h, FULFILLING|mode))) {
                    for (;;) { // loop until matched or waiters disappear
                        SNode m = s.next;       // m is s's match
                        if (m == null) {        // all waiters are gone
                            casHead(s, null);   // pop fulfill node
                            s = null;           // use new node next time
                            break;              // restart main loop
                        }
                        SNode mn = m.next;
                        //把当前节点设置为栈定节点的match属性
                        if (m.tryMatch(s)) {
                            casHead(s, mn);     // pop both s and m
                            return (E) ((mode == REQUEST) ? m.item : s.item);
                        } else                  // lost match
                            s.casNext(m, mn);   // help unlink
                    }
                }
            } else {                            // help a fulfiller
                SNode m = h.next;               // m is h's match
                if (m == null)                  // waiter is gone
                    casHead(h, null);           // pop fulfilling node
                else {
                    SNode mn = m.next;
                    if (m.tryMatch(h))          // help match
                        casHead(h, mn);         // pop both h and m
                    else                        // lost match
                        h.casNext(m, mn);       // help unlink
                }
            }
        }
    }

awaitFulfill:

SNode awaitFulfill(SNode s, boolean timed, long nanos) {

        final long deadline = timed ? System.nanoTime() + nanos : 0L;
        Thread w = Thread.currentThread();
        //自旋次数
        int spins = (shouldSpin(s) ?
                     (timed ? maxTimedSpins : maxUntimedSpins) : 0);
        for (;;) {
            if (w.isInterrupted())
                s.tryCancel();
            SNode m = s.match;
            //如果匹配节点不是空的,返回匹配到的节点
            if (m != null)
                return m;
            if (timed) {
                nanos = deadline - System.nanoTime();
                if (nanos <= 0L) {
                    s.tryCancel();
                    continue;
                }
            }
            //自旋次数-1
            if (spins > 0)
                spins = shouldSpin(s) ? (spins-1) : 0;
            else if (s.waiter == null)
                s.waiter = w; // establish waiter so can park next iter
            else if (!timed)
                //阻塞线程
                LockSupport.park(this);
            else if (nanos > spinForTimeoutThreshold)
                LockSupport.parkNanos(this, nanos);
        }
    }

注意:当前节点的match为空,就开始自旋

SNode m = s.match;

if (m != null)

return m;

达到自旋次数进行阻塞

LockSupport.park(this);

这里对源码逻辑大概梳理一下:以消费者线程先启动为例:

1.当一个消费者线程调用take方法:

2.进入transfer()方法,如果栈没有节点或者栈顶的节点也是一个消费者线程,那么就执行入队操作

3.构建新的节点放在栈顶

else if (casHead(h, s = snode(s, e, h, mode))) {

SNode m = awaitFulfill(s, timed, nanos);

...

}

4.如果没有生产者线程进行put操作,消费者线程都会压栈的方法阻塞在这里

5.当生产者线程进行put方法,也来到transfer方法

6.此时判断栈不为空且栈顶的线程类型和本线程不一样,进入else逻辑中

else if (!isFulfilling(h.mode)) { // try to fulfill

if (h.isCancelled()) // already cancelled

casHead(h, h.next); // pop and retry

else if (casHead(h, s=snode(s, e, h, FULFILLING|mode))) {

for (;😉 { // loop until matched or waiters disappear

SNode m = s.next; // m is s's match

SNode mn = m.next;

if (m.tryMatch(s)) {

casHead(s, mn); // pop both s and m

return (E) ((mode == REQUEST) ? m.item : s.item);

} else // lost match

s.casNext(m, mn); // help unlink

}

}

}

7.先构建新的节点并设置为新的栈顶节点

8.获取原先栈顶节点

9.调用m.tryMatch(s)方法 m是原先的栈顶节点,S是新的栈顶节点,这里是把新栈顶节点赋值给原先栈顶节点的match属性,并唤醒原先栈顶节点的线程

boolean tryMatch(SNode s) {

if (match == null &&

UNSAFE.compareAndSwapObject(this, matchOffset, null, s)) {

Thread w = waiter;

if (w != null) { // waiters need at most one unpark

waiter = null;

LockSupport.unpark(w);

}

return true;

}

return match == s;

}

10.消费者线程被唤醒,进行出栈操作,设置下个节点为新的栈顶,并返回元素数据

else if (casHead(h, s = snode(s, e, h, mode))) {

SNode m = awaitFulfill(s, timed, nanos);

if (m == s) { // wait was cancelled

clean(s);

return null;

}

if ((h = head) != null && h.next == s)

casHead(h, s.next); // help s's fulfiller

return (E) ((mode == REQUEST) ? m.item : s.item);

}

}

2.5 PriorityBlockingQueue

2.5.1 PriorityBlockingQueue介绍

PriorityBlockingQueue是一个无界的基于数组的优先级阻塞队列,数组的默认长度是11,虽然指定了数组的长度,但是可以无限的扩充,直到资源消耗尽为止,每次出队都返回优先级别最高的或者最低的元素。默认情况下元素采用自然顺序升序排序,当然我们也可以通过构造函数来指定Comparator来对元素进行排序。需要注意的是PriorityBlockingQueue不能保证同优先级元素的顺序。

优先级队列PriorityQueue: 队列中每个元素都有一个优先级,出队的时候,优先级最高的先出。

2.5.2 PriorityBlockingQueue应用场景

电商抢购活动,会员级别高的用户优先抢购到商品

银行办理业务,vip客户插队

2.5.3 PriorityBlockingQueue使用

代码演示:

@Slf4j

public class PriorityBlockingQueueDemo02 {

public static void main(String[] args) throws InterruptedException {

    PriorityBlockingQueue<Integer> queue=new PriorityBlockingQueue<Integer>(5);

/* //自定义Comparator

PriorityBlockingQueue queue=new PriorityBlockingQueue(

5, new Comparator() {

@Override

public int compare(Integer o1, Integer o2) {

return o2-o1;

}*/

    queue.put(1);
    queue.put(5);
    queue.put(6);
    queue.put(7);
    queue.put(2);

    log.info("结果:{}",queue.take());
    log.info("结果:{}",queue.take());
    log.info("结果:{}",queue.take());
    log.info("结果:{}",queue.take());
    log.info("结果:{}",queue.take());


}

}

运行结果:

2.5.4 PriorityBlockingQueue原理

在了解PriorityBlockingQueue原理之前,我们先补充完全二叉树和二叉堆的概念:

完全二叉树:若设二叉树)的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全二叉树。

二叉堆:二叉堆本质上是一种完全二叉树,它分为两类:最大堆和最小堆。最大堆的任何一个父节点的值都大于或等于它左右孩子节点的值;最小堆的任何一个父节点的值,都小于或等于它左右孩子节点的值

最大堆:

最小堆:

二叉堆是和数组是可以关联的.。

假设第一个元素在数组中的索引为i的话,则父节点和子节点的位置关系如下:

(01) 索引为i的左孩子的索引是 (2i+1); (02) 索引为i的右孩子的索引是 (2i+2); (03) 索引为i的父结点的索引是 ((i-1)/2);

我们的PriorityBlockingQueue就是基于二叉堆来进行排序的。接下来我们看下源码

put方法:

public void put(E e) {

offer(e); // never need to block

}

public boolean offer(E e) {

if (e == null)

throw new NullPointerException();

final ReentrantLock lock = this.lock;

lock.lock();

int n, cap;

Object[] array;

while ((n = size) >= (cap = (array = queue).length))

tryGrow(array, cap);

try {

Comparator<? super E> cmp = comparator;

if (cmp == null)

siftUpComparable(n, e, array);

else

siftUpUsingComparator(n, e, array, cmp);

size = n + 1;

notEmpty.signal();

} finally {

lock.unlock();

}

return true;

}

在这里可以看出,如果数组容量达到阈值会先扩容,如果传参传入了比较器则使用传入的比较器进行比较,否则调用siftUpComparable方法,使用自然排序

private static void siftUpComparable(int k, T x, Object[] array) {

Comparable<? super T> key = (Comparable<? super T>) x;

while (k > 0) {

//获取父节点

int parent = (k - 1) >>> 1;

//父节点元素

Object e = array[parent];

//如果比父节点大,不需要上浮,放在数组尾

if (key.compareTo((T) e) >= 0)

break;

//如果比父节点小,父节点赋值给队尾

array[k] = e;

//新节点和父节点换位置,继续比较

k = parent;

}

array[k] = key;

}

很显然,当我们添加元素的时候,先算出数组索引对应父节点的位置,然后拿到父节点上的元素和添加的元素进行比较,如果比父节点小,则换位置,依次循环,把最小的元素放在树顶。这样就完成了我们一个数组的排序,保证头节点永远是最小的。

take()方法:

public E take() throws InterruptedException {

final ReentrantLock lock = this.lock;

lock.lockInterruptibly();

E result;

try {

//这里是出队操作,出队之后唤醒阻塞的消费者线程

while ( (result = dequeue()) == null)

notEmpty.await();

} finally {

lock.unlock();

}

return result;

}

private E dequeue() {

int n = size - 1;

if (n < 0)

return null;

else {

Object[] array = queue;

//拿到最小的元素

E result = (E) array[0];

//拿到数组最后一个元素

E x = (E) array[n];

//最后一个元素置为null

array[n] = null;

Comparator<? super E> cmp = comparator;

if (cmp == null)

//开始下浮操作

siftDownComparable(0, x, array, n);

else

siftDownUsingComparator(0, x, array, n, cmp);

size = n;

return result;

}

}

这里先拿到树顶的元素,然后需要对树结构重新构造,调用siftDownComparable方法

private static void siftDownComparable(int k, T x, Object[] array,

int n) {

    if (n > 0) {
        Comparable<? super T> key = (Comparable<? super T>)x;
        int half = n >>> 1;           // loop while a non-leaf
        //每次只需要和树的一边进行比较
        while (k < half) {
            //取出当前节点左边孩子的索引位置
            int child = (k << 1) + 1; 
            //取出左边孩子元素
            Object c = array[child];
            //取出右边孩子元素
            int right = child + 1;
            //没有到队尾,且左边元素比右边元素大
            if (right < n &&
                ((Comparable<? super T>) c).compareTo((T) array[right]) > 0)
                //把右边元素置为要交换的孩子节点
                c = array[child = right];
            //如果当前节点比孩子节点都小,就不需要交换
            if (key.compareTo((T) c) <= 0)
                break;
            //否则,和最小的孩子交换节点,继续向下比。
            array[k] = c;
            k = child;
        }
        array[k] = key;
    }
}

向下堆化,就是从左儿子和右儿子之中跳出符合排序规则的点与parent调换,然后再将被调换的儿子节点作为parent节点继续执行这个过程,直至符合排序规则

2.6 DelayQueue

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

2.6.1 DelayQueue使用

@Slf4j

public class DelayQueueDemo01 {

public static void main(String[] args) throws InterruptedException {


    DelayQueue delayQueue = new DelayQueue();

    delayQueue.put(new DelayObject("A",10*1000));
    delayQueue.put(new DelayObject("B",20*1000));
    delayQueue.put(new DelayObject("C",30*1000));
    delayQueue.put(new DelayObject("D",40*1000));
    delayQueue.put(new DelayObject("E",50*1000));
    log.info("10s后获取结果");
    log.info("获取结果:{}",delayQueue.take());
    log.info("获取结果:{}",delayQueue.take());
    log.info("获取结果:{}",delayQueue.take());
    log.info("获取结果:{}",delayQueue.take());
    log.info("获取结果:{}",delayQueue.take());
}

}

class DelayObject implements Delayed {

private String name;

private long time; //延时时间

public DelayObject(String name, long delayTime) {
    this.name = name;
    this.time = System.currentTimeMillis() + delayTime;
}

@Override
public long getDelay(TimeUnit unit) {
    long diff = time - System.currentTimeMillis();
    return unit.convert(diff, TimeUnit.MILLISECONDS);
}

@Override
public int compareTo(Delayed obj) {
    if (this.time < ((DelayObject) obj).time) {
        return -1;
    }
    if (this.time > ((DelayObject) obj).time) {
        return 1;
    }
    return 0;
}

@Override
public String toString() {
    Date date = new Date(time);
    SimpleDateFormat sd = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    return "\nDelayObject:{"
            + "name=" + name
            + ", time=" + sd.format(date)
            + "}";
}

}

运行结果:

2.6.2 DelayQueue原理

数据结构:

public class DelayQueue extends AbstractQueue

implements BlockingQueue {

private final transient ReentrantLock lock = new ReentrantLock();
private final PriorityQueue<E> q = new PriorityQueue<E>();

private Thread leader = null;

private final Condition available = lock.newCondition();

}

可以看出DelayQueue内部使用PriorityQueue作为阻塞队列,使用ReentrantLock实现线程同步,并且使用了leader-fellower设计模式,如果要加入到DelayQueue,必须实现Delayed接口

put方法

public void put(E e) {

offer(e);

}

public boolean offer(E e) {

final ReentrantLock lock = this.lock;

lock.lock();

try {

q.offer(e);

if (q.peek() == e) {

leader = null;

available.signal();

}

return true;

} finally {

lock.unlock();

}

}

这里先加了一把锁,实现线程同步,然后调用优先级队列的offer方法。

q.offer(e);

public boolean offer(E e) {

if (e == null)

throw new NullPointerException();

modCount++;

int i = size;

if (i >= queue.length)

grow(i + 1);

size = i + 1;

if (i == 0)

queue[0] = e;

else

siftUp(i, e);

return true;

}

判断是否达到队列阈值,如果达到了先进行扩容,否则调用siftUp方法,

private void siftUp(int k, E x) {

if (comparator != null)

siftUpUsingComparator(k, x);

else

siftUpComparable(k, x);

}

如果设置了比较器,使用自定义的排序方法,如果没有使用默认的排序方法

private void siftUpComparable(int k, E x) {

Comparable<? super E> key = (Comparable<? super E>) x;

while (k > 0) {

int parent = (k - 1) >>> 1;

Object e = queue[parent];

if (key.compareTo((E) e) >= 0)

break;

queue[k] = e;

k = parent;

}

queue[k] = key;

}

当我们添加元素的时候,先算出数组索引对应父节点的位置,然后拿到父节点上的元素和添加的元素进行比较,如果比父节点小,则换位置,依次循环,把最小的元素放在树顶。这样就完成了我们一个数组的排序,保证头节点永远是最小的。

take方法

public E take() throws InterruptedException {

final ReentrantLock lock = this.lock;

lock.lockInterruptibly();

try {

for (;😉 {

//拿到头节点

E first = q.peek();

//头节点为空,消费者阻塞

if (first == null)

available.await();

else {

//头节点不为空,获取头节点剩余延迟时间

long delay = first.getDelay(NANOSECONDS);

//到时间,弹出队列

if (delay <= 0)

return q.poll();

//如果未到延迟时间

first = null; // don't retain ref while waiting

//这里使用leader-follower模式,已经有线程进入阻塞了

//队列其他元素肯定也没到延迟时间,直接阻塞这里。优先 级保证

if (leader != null)

available.await();

else {

//leder为空,设置leader为当前线程

Thread thisThread = Thread.currentThread();

leader = thisThread;

try {

//阻塞到延迟时间结束

available.awaitNanos(delay);

} finally {

//重新置为空

if (leader == thisThread)

leader = null;

}

}

}

}

} finally {

//如果队列还有元素,唤醒消费者线程

if (leader == null && q.peek() != null)

available.signal();

lock.unlock();

}

}

这里的核心思想就是,所有线程只有一个可以成为leader,成为leader的线程等待到延迟时间到,其他线程之间阻塞,直到leader线程释放锁。

三 如何选择合适的阻塞队列

看了这么多阻塞队列,我们如何在项目中选择合适的阻塞队列呢?

线程池对阻塞队列的选择:

● FixedThreadPool(SingleThreadExecutor 同理)选取的是 LinkedBlockingQueue

● CachedThreadPool 选取的是 SynchronousQueue

● ScheduledThreadPool(SingleThreadScheduledExecutor同理)选取的是延迟队列

而我们选择阻塞队列的时候可以从一下角度来考虑:

功能

第 1 个需要考虑的就是功能层面,比如是否需要阻塞队列帮我们排序,如优先级排序、延迟执行等。如果有这个需要,我们就必须选择类似于 PriorityBlockingQueue 之类的有排序能力的阻塞队列。

容量

第 2 个需要考虑的是容量,或者说是否有存储的要求,还是只需要"直接传递"。在考虑这一点的时候,我们知道前面介绍的那几种阻塞队列,有的是容量固定的,如 ArrayBlockingQueue;有的默认是容量无限的,如 LinkedBlockingQueue;而有的里面没有任何容量,如 SynchronousQueue;而对于 DelayQueue 而言,它的容量固定就是 Integer.MAX_VALUE。所以不同阻塞队列的容量是千差万别的,我们需要根据任务数量来推算出合适的容量,从而去选取合适的BlockingQueue。

能否扩容

第 3 个需要考虑的是能否扩容。因为有时我们并不能在初始的时候很好的准确估计队列的大小,因为业务可能有高峰期、低谷期。如果一开始就固定一个容量,可能无法应对所有的情况,也是不合适的,有可能需要动态扩容。如果我们需要动态扩容的话,那么就不能选择 ArrayBlockingQueue ,因为它的容量在创建时就确定了,无法扩容。相反,PriorityBlockingQueue 即使在指定了初始容量之后,后续如果有需要,也可以自动扩容。所以我们可以根据是否需要扩容来选取合适的队列。

内存结构

第 4 个需要考虑的点就是内存结构。我们分析过 ArrayBlockingQueue 的源码,看到了它的内部结构是"数组"的形式。和它不同的是,LinkedBlockingQueue 的内部是用链表实现的,所以这里就需要我们考虑到,ArrayBlockingQueue 没有链表所需要的"节点",空间利用率更高。所以如果我们对性能有要求可以从内存的结构角度去考虑这个问题。

性能

第 5 点就是从性能的角度去考虑。比如 LinkedBlockingQueue 由于拥有两把锁,它的操作粒度更细,在并发程度高的时候,相对于只有一把锁的 ArrayBlockingQueue 性能会更好。另外,SynchronousQueue 性能往往优于其他实现,因为它只需要"直接传递",而不需要存储的过程。如果我们的场景需要直接传递的话,可以优先考虑 SynchronousQueue。

相关推荐
zjw_rp14 分钟前
Spring-AOP
java·后端·spring·spring-aop
Oneforlove_twoforjob27 分钟前
【Java基础面试题033】Java泛型的作用是什么?
java·开发语言
TodoCoder35 分钟前
【编程思想】CopyOnWrite是如何解决高并发场景中的读写瓶颈?
java·后端·面试
向宇it44 分钟前
【从零开始入门unity游戏开发之——C#篇24】C#面向对象继承——万物之父(object)、装箱和拆箱、sealed 密封类
java·开发语言·unity·c#·游戏引擎
小蜗牛慢慢爬行1 小时前
Hibernate、JPA、Spring DATA JPA、Hibernate 代理和架构
java·架构·hibernate
星河梦瑾2 小时前
SpringBoot相关漏洞学习资料
java·经验分享·spring boot·安全
黄名富2 小时前
Redis 附加功能(二)— 自动过期、流水线与事务及Lua脚本
java·数据库·redis·lua
love静思冥想2 小时前
JMeter 使用详解
java·jmeter
言、雲2 小时前
从tryLock()源码来出发,解析Redisson的重试机制和看门狗机制
java·开发语言·数据库
TT哇2 小时前
【数据结构练习题】链表与LinkedList
java·数据结构·链表