深入理解阻塞队列BlockingQueue

阻塞队列BlockingQueue

阻塞队列与其他类型的队列不同的地方在于阻塞,即对于生产者和消费者两端来说,有任何一端的速度过快时,阻塞队列可以把过快的速度降下来。例如对于一个大小为10的阻塞队列,当生产者线程过快时,在某个时刻队列就会被装满,此时生产者线程被阻塞直到队列中有空的位置;当消费者线程过快时,在某个时刻队列是空的,此时消费者线程被阻塞直到队列中有元素。

Java中的阻塞队列的定义是BlockingQueue,它继承了队列Queue接口,我们先来了解下Queue接口。

Queue

Queue是队列的顶级接口,定义了一些出队和入队的操作,这些操作以及它们的作用如下:

  • boolean add(E e):向队列中添加元素,成功返回true,队列满时抛异常;
  • boolean offer(E e):向队列中添加元素,成功返回true,队列满时返回false;
  • E remove():删除队首的元素并返回,队列为空时抛异常;
  • E poll():删除队首的元素并返回,队列为空时返回null;
  • E element():返回队首元素,队列为空时抛异常;
  • E peek():返回队首元素,队列为空时返回null。

BlockingQueue

BlockingQueue继承了Queue接口,在其基础上添加了几个用于支持阻塞特性的方法,其中最关键的是take()方法和put()方法。

  • take():作用是获取并移除队列的头节点。当队列中有数据的时候take()方法可以正确移除;当队列中没有数据时,则阻塞,直到队列有至少一个数据。
  • put():作用是向队列中插入一个元素。当队列有空闲空间时put()方法可以正确插入;当队列已满时,则阻塞,直到队列中至少有一个空闲空间。
  • offer(E e,long timeout,TimeUnit unit):向队列中插入一个元素,可以设置阻塞时间。插入元素时如果队列已满则阻塞,超过阻塞时间返回false。
  • poll(long timeout,TimeUnit unit):获取并删除队首元素,可以设置阻塞时间。获取元素时如果队列为空则阻塞,超过阻塞时间返回null。

Queue接口和BlockingQueue接口的方法都是出队、入队或访问队首元素的方法,总结如下表:

作用\效果

不满足时抛异常

不满足时返回特定值

阻塞

阻塞指定时间

入队

add(e)

offer(e)

put(e)

offer(e,time,unit)

获取队首元素并出队

remove()

poll()

take()

poll(time,unit)

获取队首元素

element()

peek()

不支持

不支持

是否有界

阻塞队列根据其容量的大小,可以分为有界和无界两种。其中无界队列并不是真正的无界,只是表示可以容纳非常多的元素,例如LinkedBlockingQueue阻塞队列的上限是Integer.MAX_VALUE;有界队列的容量是有限的,例如ArrayBlockingQueue是由数组实现的,如果容量满了也不会扩容。

应用场景

BlockingQueue是线程安全的,因此即便生产者和消费者都是多线程的,使用阻塞队列时也不会发生线程安全问题。

队列还能起到隔离的作用,将具体任务与执行任务解耦,即将任务放到阻塞队列中,放任务的线程与执行任务的线程是不相关的,提高了安全性。

常见阻塞队列

BlockingQueue接口的实现类都在JUC包中,它们的区别主要体现在存储结构和元素操作的不同。常见的阻塞队列如下:

  • ArrayBlockingQueue:基于数组结构实现,有界阻塞队列;
  • LinkedBlockingQueue:基于链表结构,无界阻塞队列;
  • PriorityBlockingQueue:支持按优先级排序,无界阻塞队列;
  • DelayQueue:基于优先级队列,无界阻塞队列;
  • SynchronousQueue:不存储元素的阻塞队列;
  • LinkedTransferQueue:基于链表结构,无界阻塞队列;
  • LinkedBlockingDeque:基于链表结构,双端阻塞队列。

ArrayBlockingQueue

ArrayBlockingQueue是最典型的有界阻塞队列,其内部是用数组来存储元素的,因此初始化时需要指定容量大小。在生产者-消费者模型中,如果生产速度和消费速度基本匹配,可以优先考虑使用ArrayBlockingQueue。

成员变量

ArrayBlockingQueue是基于数组实现的,因此内部有一个数组类型的属性items以及表示数组实际存储元素的数量count。

final Object[] items;
int count;

ArrayBlockingQueue遵循队列“先进先出”的原则,在队尾将元素入队,在队首将元素出队,因此还需要两个“指针”来分别表示出队的数组下标和入队的数组下标。

int takeIndex;//出队指针
int putIndex;//入队指针

ArrayBlockingQueue实现线程安全的方式是使用一个ReentrantLock独占锁,由于队列为空或队列满的时候需要分别阻塞消费者线程和生产者线程,因此还需要两个条件队列。

final ReentrantLock lock;
private final Condition notEmpty;//当队列为空时,阻塞消费者线程
private final Condition notFull;//当队列已满时,阻塞生产者线程

构造函数

ArrayBlockingQueue由于是基于数组实现的,因此至少要传入数组的容量大小。

public ArrayBlockingQueue(int capacity) {
    this(capacity, false);
}

另外,还可以借用ReentrantLock来实现公平和非公平的阻塞队列,因此还提供了是否公平的参数。

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

ArrayBlockingQueue还提供了一个可以直接将已有元素的集合转成阻塞队列的构造函数。

public ArrayBlockingQueue(int capacity, boolean fair,
                              Collection 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();
    }
}

put()和take()

ArrayBlockingQueue阻塞队列核心的方法就是put()方法和take()方法,此处我们只对这两个方法做详细介绍。

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();
    }
}
  1. 为了保证阻塞队列的线程安全,首先使用ReentrantLock加锁;
  2. 通过count == items.length判断队列是否已满,如果已满则阻塞当前生产者线程,需要注意的是调用await()方法时需要使用while关键字来判断条件是否已满足,因为进入条件等待队列的线程在被signal()唤醒进入同步队列时虽然条件是满足的,但当该线程从同步等待队列再获取锁执行时可能就不满足了,如果直接向下运行就会出现问题;
  3. 调用enqueue()方法将指定元素入队;
  4. 最后调用unlock()方法解锁。

enqueue()入队方法的实现如下。

private void enqueue(E x) {
    final Object[] items = this.items;
    items[putIndex] = x;
    if (++putIndex == items.length)
        putIndex = 0;
    count++;
    notEmpty.signal();
}
  1. 直接将指定元素赋值到数组下标为putIndex的位置中,从此处可以看出putIndex指向的位置是下一次要添加的元素的位置;
  2. 将putIndex加1,并判断改变后的putIndex是否已到达数组的结尾;
  3. 如果是,则将putIndex重置为0,这表示ArrayBlockingQueue的数组是一个环形数组;
  4. 将表示数组元素数量的count加1;
  5. 入队成功后表示当前阻塞队列中至少有一个元素,因此调用notEmpty的signal()方法来将可能在阻塞的消费者线程转移到同步等待队列中。

take()方法的作用是返回并移除队首的元素,如果队列为空,则阻塞当前消费者线程,实现如下:

public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == 0)
            notEmpty.await();
        return dequeue();
    } finally {
        lock.unlock();
    }
}
  1. 首先使用ReentrantLock加锁;
  2. 判断count如果为0,即当队列为空时,直接将当前消费者线程阻塞在notEmpty条件变量上,等待put()方法调用notEmpty.signal()方法唤醒;
  3. 调用dequeue()方法将队首元素出队;
  4. 调用unlock()方法释放锁。

dequeue()出队方法的实现如下。

private E dequeue() {
    final Object[] items = this.items;
    @SuppressWarnings("unchecked")
    E x = (E) items[takeIndex];
    items[takeIndex] = null;
    if (++takeIndex == items.length)
        takeIndex = 0;
    count--;
    if (itrs != null)
        itrs.elementDequeued();
    notFull.signal();
    return x;
}
  1. 首先取出数组下标为takeIndex的数组元素,并将该下标表示的数组元素置为null;
  2. 将takeIndex加1,判断takeIndex是否已到达数组最后,同样是为了实现环形数组;
  3. 将count减1;
  4. 调用notFull.signal()方法将可能在阻塞的生产者线程转移到同步等待队列。

总结

ArrayBlockingQueue是一个基于静态数组实现的有界阻塞队列,遵循“先进先出”的原则。静态数组表示队列长度是固定的,没有扩容机制。另外在静态数组中没有元素的位置存储的是null,这一点从上面的源码也可以看出来。

ArrayBlockingQueue使用了一个ReentrantLock锁,这意味着其存取操作加的都是同一把锁,即其存取操作不能同时进行,存储操作排斥。

ArrayBlockingQueue在入队时从队尾(下标putIndex处)添加元素,如果当前队列已满则调用notFull.await()将当前生产者线程阻塞,否则将putIndex的值加1,如果此时putIndex的值等于数组大小,则将其重置为0,最后由于队列中已有至少一个元素,调用notEmpty.signal()将可能阻塞的消费者线程转移到同步等待队列;在出队时从队首(下标takeIndex处)取出元素,如果当前队列为空则调用notEmpty.signal()将当前消费者线程阻塞,否则将takeIndex的值加1,如果此时takeIndex的值等于数组大小,则将其重置为0,最后由于队列中至少有一个空闲空间,调用notFull.signal()将可能阻塞的生产者线程转移到同步等待队列。

LinkedBlockingQueue

LinkedBlockingQueue是基于单链表实现的无界阻塞队列,也可以通过传入参数来指定队列大小。

成员变量

由于可以通过传入参数来指定LinkedBlockingQueue队列的大小,因此需要有属性来存储其大小,另外同样需要存储实际的元素数量。

private final int capacity;//队列大小
private final AtomicInteger count = new AtomicInteger();//实际存储的元素数量

LinkedBlockingQueue是基于单链表实现的,保存单链表的首节点和尾节点也是必要的。

transient Node head;//链表首节点
private transient Node last;//链表尾节点

ArrayBlockingQueue只使用了一个ReentrantLock锁,因此其存取操作是互斥的,这在性能上多少会有一些影响。而LinkedBlockingQueue在此处做了改进,为存操作和取操作分别使用了一个ReentrantLock锁。

private final ReentrantLock takeLock = new ReentrantLock();//取锁
private final ReentrantLock putLock = new ReentrantLock();//存锁

由于条件变量Condition是与ReentrantLock锁关联的(必须在与其对应的锁内才能执行相关阻塞和唤醒方法),因此用于阻塞消费者线程的notEmpty条件变量需要由取锁来获取,用于阻塞生产者线程的notFull条件变量需要由存锁来获取。

private final Condition notEmpty = takeLock.newCondition();//取锁获取用于阻塞消费者线程的条件变量
private final Condition notFull = putLock.newCondition();//存锁获取用于阻塞生产者线程的条件变量

构造函数

LinkedBlockingQueue默认情况下的容量是int的最大值,其无参构造函数实现如下。

public LinkedBlockingQueue() {
    this(Integer.MAX_VALUE);
}

也可以通过传入一个小于int最大值的参数来指定队列大小,这时LinkedBlockingQueue就是一个有界阻塞队列,除此外,在构造函数中将单链表的头尾节点初始化,都指向一个节点。

public LinkedBlockingQueue(int capacity) {
    if (capacity <= 0) throw new IllegalArgumentException();
    this.capacity = capacity;
    last = head = new Node(null);
}

LinkedBlockingQueue也可以通过传入一个有值的集合来转换为阻塞队列。

public LinkedBlockingQueue(Collection 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();
    }
}

put()和take()

put()方法在LinkedBlockingQueue队列的实现如下。

public void put(E e) throws InterruptedException {
    if (e == null) throw new NullPointerException();
    int c = -1;
    Node node = new Node(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)
            notFull.signal();
    } finally {
        putLock.unlock();
    }
    if (c == 0)
        signalNotEmpty();
}

private void signalNotEmpty() {
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lock();
    try {
        notEmpty.signal();
    } finally {
        takeLock.unlock();
    }
}
  1. 将要入队的元素封装到Node对象中;
  2. 使用putLock存锁加锁;
  3. 判断如果当前队列存储的元素数量是否与队列容量相同,如果相同则阻塞当前生产者线程,否则继续执行;
  4. 调用enqueue()方法将node节点入队;
  5. 将count加1,如果计算后的count的值仍小于队列容量,则调用notFull.signal()方法唤醒阻塞的生产者线程;
  6. 调用unlock()方法解锁;
  7. 判断c(count计算之前的值,即此次put()方法之前队列存储元素的数量)如果为0,则唤醒可能阻塞的消费者线程,需要注意的是此处仅会在队列为空执行put()添加元素时,后续再也不会再唤醒消费者线程。

enqueue()入队方法的实现如下,单链表的入队比较简单,只需要将队尾的next指针和last指针先后指向当前node节点即可。

private void enqueue(Node node) {
    last = last.next = node;
}

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();
    }
    if (c == capacity)
        signalNotFull();
    return x;
}

private void signalNotFull() {
    final ReentrantLock putLock = this.putLock;
    putLock.lock();
    try {
        notFull.signal();
    } finally {
        putLock.unlock();
    }
}
  1. 获取takeLock取锁,加锁;
  2. 判断cout是否为0,如果为0则阻塞当前消费者线程;
  3. 否则执行dequeue()方法将队首元素出队;
  4. 判断c是否大于1,如果大于则调用notEmpty.signal()唤醒可能在阻塞的消费者线程;
  5. 调用unlock()方法解锁;
  6. 判断执行此次take()方法前count的值是否为队列的容量(即判断队列是否是满的),如果是则唤醒可能在阻塞的生产者线程,需要注意的是take()方法仅会在队列已满执行take()方法时才会唤醒阻塞的生产者线程。

dequeue()出队方法的实现如下,需要注意的是head指针指向的是构造函数初始化时的node节点,第一次执行出队时移除的也是该node节点,但返回的是head的next指针指向的第一个节点。最后将head指针指向队列的第一个节点,并将其节点的值item置为null。

private E dequeue() {
    Node h = head;
    Node first = h.next;
    h.next = h; // help GC
    head = first;
    E x = first.item;
    first.item = null;
    return x;
}

总结

LinkedBlockingQueue是基于单向链表实现的无界阻塞队列,遵循“先进先出”的原则。可以在构造函数中传入指定容量来使其变为有界阻塞队列,并且无界也是有最大值的,最大值为Integer.MAX_VALUE。

LinkedBlockingQueue为存操作和取操作分别都分配了一把ReentrantLock锁,因此其存取操作可以并发执行,效率比存取互斥的ArrayBlockingQueue高。这也是线程池中使用LinkedBlockingQueue而不使用ArrayBlockingQueue的原因。LinkedBlockingQueue还有一点优于ArrayBlockingQueue的地方在于:ArrayBlockQueue在每次put()和take()后都会调用notEmpty或者notFull的signal()方法来唤醒可能存在阻塞的消费者线程或者生产者线程,而ArrayBlockingQueue只会在队列为空时唤醒消费者线程、队列刚好不满时唤醒生产者线程,避免了不必要的线程调度。

LinkedBlockingQueue在队尾的last指针处入队,在队首的head指针处出队。

LinkedBlockingQueue在执行删除方法remove()时存锁和取锁都会加锁解锁。

ArrayBlockingQueue和LinkedBlockingQueue的区别

  • 队列大小不同。ArrayBlockingQueue是基于数组实现的,因此它是有界的且初始化时必须指定大小;LinkedBlockingQueue可以是有界的也可以是无界的,无界时其大小是Integer.MAX_VALUE,因此当其无界且生产速度大于消费速度时,有可能会有内存溢出问题。
  • 使用的锁数量不同。ArrayBlockingQueue只适用了一把ReentrantLock锁,因此其存取操作时互斥的;LinkedBlockingQueue是锁分离的,为存操作和取操作分别使用了一把ReentrantLock锁,存取操作可以并发执行。因此LinkedBlockingQueue的性能相较于ArrayBlockingQueue较高。
  • 空间占用不同。ArrayBlockingQueue是基于数组实现的,在插入或删除元素时不会产生或销毁任何额外的对象实例;LinkedBlockingQueue是基于链表实现的,在插入元素时会生成一个额外的Node对象。因此在长时间需要高效并发地处理大批量数据时,LinkedBlockingQueue对GC的性能影响更大。

LinkedBlockingDeque

LinkedBlockingDeque是基于双向链表实现的无界阻塞队列,可以指定容量变为有界阻塞队列。LinkedBlockingDeque与LinkedBlockingQueue很相似,主要有以下不同:

  • 锁数量:LinkedBlockingQueue有存锁和取锁两个ReentrantLock锁;LinkedBlockingDeque只有一把锁,因此其存取互斥;
  • API:LinkedBlockingDeque具有更丰富的API,在其队首和队尾都实现了对应的添加和删除元素的API;LinkedBlockingQueue只有从队首删除元素以及队尾添加元素的API。

SynchronousQueue

SynchronousQueue是基于链表实现的没有数据缓存的阻塞队列,其队列容量为0,它只是多个线程之间数据交换的媒介。SynchronousQueue最大的特点是生产者线程和消费者线程需要同步地存取数据,当只有生产者线程访问阻塞队列时会被阻塞,直到消费者线程访问阻塞队列获取到该生产者线程的数据;当只有消费者线程访问阻塞队列时也会被阻塞,直到生产者线程访问阻塞队列提供数据给该消费者线程。

SynchronousQueue有公平模式和非公平模式的实现。公平模式使用的是队列数据结构,遵循“先进先出”的原则;非公平模式使用的是栈

数据结构,遵循“先进后出”的原则。

Transferer

Transferer是SynchronousQueue中的一个抽象内部类,定义了抽象方法transfer(),SynchronousQueue的存取操作都调用了该方法。SynchronousQueue的公平模式和非公平模式分别定义了一个类且继承了Transferer类,其中公平模式的类是TransferQueue,非公平模式的类是TransferStack。

构造函数

SynchronousQueue分为公平模式和非公平模式,默认情况是非公平模式。

public SynchronousQueue() {
    this(false);
}
public SynchronousQueue(boolean fair) {
    transferer = fair ? new TransferQueue() : new TransferStack();
}

put()和take()

SynchronousQueue的put()方法和take()方法的主要逻辑都在transfer()方法中,只是在调用transfer()时put()方法需要传递生产者数据,take()没有传递数据而已。

put()方法实现如下:

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

take()方法实现如下:

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

公平模式

公平模式的Transferer的实现是TransferQueue类,使用的是队列数据结构,为了维护此数据结构在其内部定义了QNode链表节点类。QNode的作用是存储生产者线程或消费者线程信息,其主要属性包含以下几种:

  • QNode next:指向下一个节点的指针;
  • Object item:存储生产者线程要传递的数据或消费者线程要接收的数据;
  • Thread waiter:存储生产者线程或消费者线程;
  • boolean isData:为true时表示当前线程是生产者线程,false时表示当前线程是消费者线程。

TransferQueue的构造函数实现如下,其中head为单链表的头节点,tail为尾节点。

TransferQueue() {
    QNode h = new QNode(null, false); // initialize to dummy node.
    head = h;
    tail = h;
}
transfer()

由于SynchronousQueue的put()和take()调用的都是transfer()方法,因此我们主要介绍transfer()。公平模式的transfer()方法的实现如下:

E transfer(E e, boolean timed, long nanos) {
    QNode s = null; /
    boolean isData = (e != null);
    for (;;) {
        QNode t = tail;
        QNode h = head;
        if (t == null || h == null)     
            continue;                    
        if (h == t || t.isData == isData) {
            QNode tn = t.next;
            if (t != tail)            
                continue;
            if (tn != null) {               
                advanceTail(t, tn);
                continue;
            }
            if (timed && nanos <= 0)      
                return null;
            if (s == null)
                s = new QNode(e, isData);
            if (!t.casNext(null, s))    
                continue;
            advanceTail(t, s);             
            Object x = awaitFulfill(s, e, timed, nanos);
            if (x == s) {                 
                clean(t, s);
                return null;
            }
            if (!s.isOffList()) {         
                advanceHead(t, s);         
                if (x != null)              
                    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;                  
            Object x = m.item;
            if (isData == (x != null) ||  
                x == m ||                  
                !m.casItem(x, e)) {       
                advanceHead(h, m);         
                continue;
            }
            advanceHead(h, m);          
            LockSupport.unpark(m.waiter);
            return (x != null) ? (E)x : e;
        }
    }
}

Object awaitFulfill(QNode s, E e, boolean timed, long nanos) {
    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);
    }
}

当前SynchronousQueue阻塞队列首次被一个生产者线程或一个消费者线程访问时会经历以下步骤:

  1. 首先判断入参e是否为空,不为空则表示当前线程是生产者线程(put()方法调用时传入的参数),为空则表示当前线程是消费者线程(take()方法调用时未传参数);
  2. 判断链表元素是否为空或当前要入队的线程的模式与tail节点的模式是否相同(即最后一次调用该transfer()方法且入队成功的线程与当前调用transfer()方法是否都是生产者线程或消费者线程),此时链表元素为空;
  3. 将当前线程、item(生产者线程item是要传递的数据,消费者线程item为null)和线程的模式(是否生产者线程)封装为QNode节点对象,并入队;
  4. 判断当前QNode节点是否为head的下一个节点,如果是则使用CAS+自旋;否则直接调用LockSupport.park()方法将当前线程阻塞。

此时再使用一个与上面的模式相同的线程(生产者线程或消费者线程)再次调用transfer()方法,所经历的步骤与上面一样。如果这两次调用都是生产者线程调用put(),则目前就有两个生产者线程在阻塞,否则有两个消费者线程在阻塞。

随后再使用一个与前两次调用模式不同的线程调用transfer()方法,步骤1与上面相同,从步骤2开始有区别,具体步骤如下:

  1. 判断链表元素是否为空或当前要入队的线程的模式与tail节点的模式是否相同,这两处都为false;
  2. 如果当前线程是消费者线程调用的take()方法,队列前面阻塞的两个线程是生产者线程,则将队首的生产者线程出队并唤醒,返回队首生产者线程的item值给当前消费者线程;如果当前线程是生产者线程调用的put()方法,队列前面阻塞的两个线程是消费者线程,则将队首的消费者线程的item设置为当前生产者线程的item,将其出队并唤醒,当前生产者线程返回,队首的消费者线程获取锁后返回其本身的item值。

PriorityBlockingQueue

PriorityBlockingQueue是一个基于数组+二叉堆实现的支持按照优先级排序的无界阻塞队列,优先级高的先出队,优先级低的后出队。其中优先级取决于在初始化PriorityBlockingQueue时是否传入比较器,如果传入了比较器则按照比较器给定的顺序排序,否则就按照自然顺序排序。

PriorityBlockingQueue只使用了一把ReentrantLock锁,即它的存取操作是互斥的。

PriorityBlockingQueue在入队时不阻塞,这一点与前面介绍的阻塞队列稍有不同;出队时直接将堆顶的元素出队即可,因此堆顶的元素的优先级是最高的(例如最小堆的堆顶的元素的值是最小的,最大堆的堆顶的元素的值是最大的)。

DelayQueue

DelayQueue是基于优先级队列PriorityQueue实现的无界阻塞队列,对于DelayQueue来说延时越低优先级越高,因此先出队的是延时低的元素。

如何选择合适的阻塞队列

通常可以从以下五个角度来选择合适的阻塞队列:

  • 功能:首先考虑我们是否需要阻塞队列帮我们完成某种功能,例如PriorityBlockingQueue可以帮助我们按照优先级排序等,如果需要就选择具有对应功能的阻塞队列;
  • 容量:其次需要根据业务场景需要来计算任务数量,推断出需要的容量,从而选择或者有限容量的,或者无限容量的阻塞队列;
  • 能否扩容:有时在开始并不能准确的估算出所需要的队列容量大小,因为业务可能有高峰期、低谷期。因此可能需要阻塞队列有动态扩容的功能,对于一些不能动态扩容的例如ArrayBlockingQueue阻塞队列等就可以排除在外了;
  • 内存结构:基于数组实现的ArrayBlockingQueue在空间上是优于链表实现的LinkedBlockingQueue的,可以从内存结构的角度来分析;
  • 性能:LinkedBlockingQueue拥有两把锁,相比于只有一把锁的ArrayBlockingQueue性能更好,而SynchronousQueue由于不需要存储,性能又优于其他阻塞队列的实现。

你可能感兴趣的:(并发编程,java)