Depth analysis of concurrent blocking queue

Part blog, we talk about the thread pool workQueue, task queue

private final BlockingQueue<Runnable> workQueue;
复制代码

You can see a BlockingQueue type, BlockingQueue is the interface, we actually used a variety of concurrent queue BlockingQueue implementation. We have children one by one to check it out now

BlockingQueue

public interface BlockingQueue<E> extends Queue<E> {}
复制代码

BlockingQueue inherited from the Queue, so to meet the FIFO. There are a lot of source introduced BlockingQueue front, conclude that BlockingQueue insertion operation, removing operation, acquires the operating element provides four different methods used for the different scenarios: 1, an exception is thrown; 2, the special return value (null or true / false, depending on the particular operation); 3, this block waiting for operation until the operation is successful; 4, this block waiting for operation until a specified time or a timeout success

BlockingQueue inserted null values ​​are not accepted

BlockingQueue is designed to achieve producer - consumer queue.

BlockingQueue achieve are thread-safe, but the bulk of the collection operations as addAll, containsAll, retainAll and removeAll are not necessarily atomic operation. The addAll (c) it is possible to add some elements after the middle of an exception is thrown, then BlockingQueue some elements have been added, this is permissible, depending on the particular implementation.

Well, this is just the interface, we ado, you must go to achieve it

ArrayBlockingQueue

As the name suggests, it is the underlying implementation is an array

// 用于存放元素的数组
final Object[] items;

// 下一次读取操作的位置
int takeIndex;

// 下一次写入操作的位置
int putIndex;

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

// 锁保证出入队原子性
final ReentrantLock lock;

// 出队同步,等待满足非空
private final Condition notEmpty;

// 入队同步,等待满足非满
private final Condition notFull;
复制代码

Constructor

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();
    }
复制代码

You can see, the default is to use non-ReentrantLock fair locks.

offer

public boolean offer(E e) {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            if (count == items.length)
                return false;
            else {
                enqueue(e);
                return true;
            }
        } finally {
            lock.unlock();
        }
}

private void enqueue(E x) {
       
        final Object[] items = this.items;
        
        // 元素入队
        items[putIndex] = x;
        // putIndex达到最大,调整
        if (++putIndex == items.length)
            // 队列前边的已被取走,所以putIndex达到最大可以再从0开始
            putIndex = 0;
        count++;
        // 此时满足非空,可以唤醒一个notEmpty等待队列上的元素
        notEmpty.signal();
}
复制代码
  1. Ensure non-null elements
  2. Get lock
  3. If the queue is full, it returns false, adding failure; otherwise, add successful, returns true
  4. The lock is released, the release said front lock semantics, to modify the shared variable values ​​brush back to main memory

Note that the number of elements is equal to a queue full count, instead of using the put / takeindex Analyzing

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();
        }
    }
复制代码

Nothing to say, if the queue is full on blocking hangs in the queue on condition notFull

poll

public E poll() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return (count == 0) ? null : 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;
        count--;
        if (itrs != null)
            itrs.elementDequeued();
        // 此时取走一个元素,可以唤醒notFull上的一个线程    
        notFull.signal();
        return x;
}
复制代码

I can see nothing more than to take the lock, and then take away the element, update takeIndex, wake botFull, and finally release the lock.

take

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

Compared poll, take the queue is empty blocks the hang time, poll is not blocked direct return null

peek

public E peek() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return itemAt(takeIndex); 
        } finally {
            lock.unlock();
        }
}

final E itemAt(int i) {
        return (E) items[i];
    }
复制代码

The same peek not blocked, directly returned to the queue header element; empty, then return null

size

public int size() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return count;
        } finally {
            lock.unlock();
        }
    }
复制代码

Get a lock to return to count, the lock is released

ArrayBlockingQueue summary

ArrayBlockingQueue only take and put it is blocked, other methods are direct return, while you can see ArrayBlockingQueue the lock granularity is a great global lock, each operating methods need to acquire a lock, similar to the synchronized hashtable, but also because of this, where the size is accurate statistics

LinkedBlockingQueue

Or the name implies, it is the bottom of the list

Attributes

private final int capacity;

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

    // 链表头
    transient Node<E> head;

    // 链表尾
    private transient Node<E> last;

    //  take,poll, peek 等读操作的方法需要获取到这个锁
    private final ReentrantLock takeLock = new ReentrantLock();

    // 等非空条件满足的condition
    private final Condition notEmpty = takeLock.newCondition();

    // put, offer 等写操作的方法需要获取到这个锁
    private final ReentrantLock putLock = new ReentrantLock();

    // 等不满条件满足的condition
    private final Condition notFull = putLock.newCondition();
复制代码

We can see that there are write locks and read locks, and so the operation will not have to read between concurrency issues between write operations, then write between read it? We introduced under

Construction method

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

public LinkedBlockingQueue(int capacity) {
    if (capacity <= 0) throw new IllegalArgumentException();
    this.capacity = capacity;
    last = head = new Node<E>(null);
}
复制代码

Constructor initializes a head node, then the first element into the team when the queue will have two elements. Reading element, always acquires a node behind the head node. Count value COUNT is not including the head node.

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 {
            // 如果队列满了,等待notFull条件满足
            while (count.get() == capacity) {
                notFull.await();
            }
            // 满足notFull,新增节点入队
            enqueue(node);
            c = count.getAndIncrement();
            if (c + 1 < capacity)
                // 如果此时仍然没满,唤醒等待在notFull上的一个线程
                notFull.signal();
        } finally {
            putLock.unlock();
        }
        if (c == 0)
            // c==0即元素入队前是空的,那么所有的读线程都在等待 notEmpty 这个条件,等待唤醒,这里做一次唤醒操作
            signalNotEmpty();
}

private void enqueue(Node<E> node) {
        // 让last指向新增元素,注意是在获取锁的情况下进行的,不存在并发问题
        last = last.next = node;
}
复制代码

If the queue is full to wait dissatisfaction, it satisfies condition into the team.

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();
        }
        c==capacity的话,此时去走一个,就可以唤醒阻塞在notFull上的线程了
        if (c == capacity)
            signalNotFull();
        return x;
}


// 这里直接把头节点置null,且指向自己,头节点的next值为null,变为新的头节点
private E dequeue() {
        Node<E> h = head;
        Node<E> first = h.next;
        h.next = h; // help GC
        head = first;
        E x = first.item;
        first.item = null;
        return x;
}
复制代码

If the queue is empty waiting nonempty, remove the head of the queue to satisfy the condition

size

public int size() {
        return count.get();
}
复制代码

The count is not very accurate.

Other similar methods are no longer blocked

LinkedBlockingQueue summary

Internal LinkedBlockingQueue implemented by a single linked list, using head and tail node enqueue and dequeue operations, respectively two locks, so that enqueuing and dequeuing can be performed simultaneously.

SynchronousQueue

Continue the name suggests, the synchronization queue, what is synchronized? This refers to when a thread is writing an element to the queue, the write operation does not return immediately, you need to wait for another thread to this element away; the same token, when a reader thread to do the read operation, the same write operation requires writing a matching thread. Synchronous here refers to read and write threads need to synchronize threads , a thread matching a read write thread.

So in fact this queue does not exist, it does not provide any space (not one) to store the elements. Data must be read to a thread from a thread writing, rather than written to a queue waiting to be consumed.

Construction method

public SynchronousQueue(boolean fair) {
        transferer = fair ? new TransferQueue<E>() : new TransferStack<E>();
    }
复制代码

You can see is actually initiated a transferer, you can guess where fair is fair and lock it, lock it fair if initialization TransferQueue, otherwise initialization TransferStack, As this is something to look at for a while.

The old rules, the old look put / take it

put

// 写入值
public void put(E o) throws InterruptedException {
    if (o == null) throw new NullPointerException();
    if (transferer.transfer(o, false, 0) == null) { // 1
        Thread.interrupted();
        throw new InterruptedException();
    }
}
// 读取值并移除
public E take() throws InterruptedException {
    Object e = transferer.transfer(null, false, 0); // 2
    if (e != null)
        return (E)e;
    Thread.interrupted();
    throw new InterruptedException();
}
复制代码

put / call is internal take Transferer.transfer (...) method, except that the first parameter is a null value.

We see, write put (E o) and read operations take () is invoked Transferer.transfer (...) method, except that the first argument is null value. Let's look at the design ideas transfer, the basic algorithm is as follows: When this method is called consistent if the queue is empty, or queue nodes and the current thread type of operation (such as the current operation is a put operation, while the queue elements are also write thread). In this case, the current thread can be added to the waiting queue. If there are nodes wait queue, and the current operation can be matched (e.g., are read queue thread, the current thread is the thread write operation, and vice versa). In this case, matching the HOL queue, dequeued return the corresponding data. In fact, there's a hidden condition is met, if the queue is not empty, are certainly the same type of nodes are either read or write operations are. This depends in the end is to read a thread backlog, or write thread backlog. We can assume that the scene of a pair of men and women: a man over, if a person did not, then he needs to wait; if there are a bunch of men in waiting, then he needs routed behind the queue; if found to be a bunch of woman in the queue, then he is directly led by the nose team head of the woman.

Source code for this child is not introduced, on top of what we understand enough, because this queue itself is rarely used, and only the thread pool will be used.

PriorityBlockingQueue

Priority queue to implement priority to relate to compare the size, the insertion of the queue object must compare the size of the class. The same can not be inserted null.

In fact, it can be said that he is unbounded queue, because his size is not fixed, support expansion of the operation, so put method will not block

Attributes

// 构造方法中,如果不指定大小的话,默认大小为 11
private static final int DEFAULT_INITIAL_CAPACITY = 11;
// 数组的最大容量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

// 这个就是存放数据的数组
private transient Object[] queue;

// 队列当前大小
private transient int size;

// 大小比较器,如果按照自然序排序,那么此属性可设置为 null
private transient Comparator<? super E> comparator;

// 并发控制所用的锁,所有的 public 且涉及到线程安全的方法,都必须先获取到这个锁
private final ReentrantLock lock;

private final Condition notEmpty;

// 这个也是用于锁,用于数组扩容的时候,需要先获取到这个锁,才能进行扩容操作
// 其使用 CAS 操作
private transient volatile int allocationSpinLock;
复制代码

Construction method

// 默认构造方法,采用默认值(11)来进行初始化
public PriorityBlockingQueue() {
    this(DEFAULT_INITIAL_CAPACITY, null);
}
// 指定数组的初始大小
public PriorityBlockingQueue(int initialCapacity) {
    this(initialCapacity, null);
}
// 指定比较器
public PriorityBlockingQueue(int initialCapacity,
                             Comparator<? super E> comparator) {
    if (initialCapacity < 1)
        throw new IllegalArgumentException();
    this.lock = new ReentrantLock();
    this.notEmpty = lock.newCondition();
    this.comparator = comparator;
    this.queue = new Object[initialCapacity];
}
复制代码

Expansion

private void tryGrow(Object[] array, int oldCap) {
        // 释放了原来的独占锁 lock,这样的话,扩容操作和读操作可以同时进行,提高吞吐量
        lock.unlock(); 
        Object[] newArray = null;
        
        // 获取到allocationSpinLock才能进行数组扩容
        if (allocationSpinLock == 0 &&
            UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset,
                                     0, 1)) {
            try {
                // 如果节点个数小于 64,那么增加的 oldCap + 2 的容量
                // 如果节点数大于等于 64,那么增加 oldCap 的一半
                // 所以节点数较小时,增长得快一些
                int newCap = oldCap + ((oldCap < 64) ?
                                       (oldCap + 2) : 
                                       (oldCap >> 1));
                // 可能溢出                       
                if (newCap - MAX_ARRAY_SIZE > 0) {   
                    int minCap = oldCap + 1;
                    if (minCap < 0 || minCap > MAX_ARRAY_SIZE)
                        throw new OutOfMemoryError();
                    newCap = MAX_ARRAY_SIZE;
                }
                // 如果 queue != array,那么说明有其他线程给 queue 分配了其他的空间
                if (newCap > oldCap && queue == array)
                    newArray = new Object[newCap];
            } finally {
                allocationSpinLock = 0;
            }
        }
        // 其他线程做了扩容操作
        if (newArray == null) 
            Thread.yield();
        lock.lock();
        // 将原来数组中的元素复制到新分配的大数组中
        if (newArray != null && queue == array) {
            queue = newArray;
            System.arraycopy(array, 0, newArray, 0, oldCap);
        }
    }
复制代码

put

public void put(E e) {
    // 直接调用 offer 方法,在这里,put 方法不会阻塞
    offer(e); 
}
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
        size = n + 1;
        // 唤醒等待的读线程
        notEmpty.signal();
    } finally {
        lock.unlock();
    }
    return true;
}
复制代码

You can see, the process is very simple, get a lock, insert elements, adjusting the binary heap, pile on the limited space, we will write back article details, not repeat them here.

take

public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    // 独占锁
    lock.lockInterruptibly();
    E result;
    try {
        // dequeue 出队
        while ( (result = dequeue()) == null)
            notEmpty.await();
    } finally {
        lock.unlock();
    }
    return result;
}
复制代码

The same is to acquire an exclusive lock, if the queue is empty, then, need to wait, if not empty directly out, attention also needs to adjust the binary heap.

DelayQueue

As the name suggests, it is a delay queue, what does that mean? That is, each element has expired queue time, queue elements from the acquisition can only get expired elements

It can be applied to the following scenarios:

  • Design caching system using this cache element is valid queue, open a query queue thread, once acquired, it represents valid until the cache
  • Use this queue scheduling regular tasks to save time and performs tasks of the day, once you get to perform tasks, such as TimeQueue

The queue Delayed elements must implement the interface, and must implement compareTo method specified element order, such that the delay element at the end of the longest queue

public interface Delayed extends Comparable<Delayed> {
    long getDelay(TimeUnit unit);
}
复制代码

The method returns the current element how much time is available, to obtain a delay element in the queue, if the element does not reach the delay time to occlusion.

to sum up

It is a very, very long article. . .

  • ArrayBlockingQueue bottom is an array, bounded queue, if we want to use producer - consumer model, which is a very good choice.
  • LinkedBlockingQueue ground floor is the list, it can be used as unbounded and bounded queue to use, so we do not think it is the unbounded queue.
  • SynchronousQueue itself with no space to store any element may be selected mode and a non fair fair mode use.
  • PriorityBlockingQueue unbounded queue, array based, binary heap data structure, a tree is a root node of the first array are always minimum.
  • DelayQueue is also unbounded blocking queue, use PriorityQueue as the underlying implementation, can only delay the expiration of getting elements

No public programmer Ergou

Original articles per day, with the exchange of learning

Guess you like

Origin juejin.im/post/5db6bc97f265da4d525fce20