(juc Series) synchronization queue

The source code of this article is based on JDK13

SynchronousQueue

Official annotation translation

For the implementation of a blocking queue, its insertion operation must wait for the corresponding removal operation. Vice versa

A synchronization queue has no internal capacity limit

  • The peek() operation cannot be performed, so the element exists only when you try to remove it
  • You cannot use any method to insert an element when there are no other threads waiting to be removed
  • The iteration operation cannot be performed because no element can be iterated

The queue header element is the first write thread to attempt to add an element; If there is no waiting write thread, no element can be removed, and the poll method will return null

If SynchronousQueue is viewed from a collection perspective, it is an empty collection. This queue also does not accept null elements

Synchronization queue is like a merge channel. Things running in one thread must synchronously wait for transactions running in another thread to process some information, such as events, tasks, etc

This class supports optional fair policies. By default, there is no guarantee. If the fair policy is specified in the constructor, the order of thread access will be FIFO

This class is also part of the Java collection framework

Source code

definition

public class SynchronousQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable {

It inherits AbstractQueue and BlockingQueue, so it is a blocking queue

attribute

// Class responsible for transmission
private transient volatile Transferer<E> transferer;
// Queue lock
private ReentrantLock qlock;
// Producer waiting queue
private WaitQueue waitingProducers;
// Consumer's waiting queue
private WaitQueue waitingConsumers;

There are four attributes in total. Except for one reentrant lock, the others are internal implementation classes. Take a look in turn

Transferer

// Abstract class, which defines the behavior of transmission. Its implementation class is shown below
abstract static class Transferer<E> {
    abstract E transfer(E e, boolean timed, long nanos);
}
TransferStack stack

The head node of the stack is saved internally: head

volatile SNode head;

This SNode is also an internal class, which is relatively simple

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

It saves the value of the current node, the next node, and the node matching the current node

At the same time, the waiting thread is saved for blocking and waking up

The implementation of TransferStack stack for transfer is as follows:

        @SuppressWarnings("unchecked")
        E transfer(E e, boolean timed, long nanos) {
            SNode s = null; // constructed/reused as needed
                // Is the type of current request producer or consumer
            int mode = (e == null) ? REQUEST : DATA;

            // spin
            for (;;) {
                SNode h = head;
                // If the current stack is empty, or the type of the first element of the stack is different from the current type
                if (h == null || h.mode == mode) {  // empty or same-mode
                    // Timeout
                    if (timed && nanos <= 0L) {     // can't wait
                        // The header node has timed out. Pop up the header node and let the next node become the header node
                        if (h != null && h.isCancelled())
                            casHead(h, h.next);     // pop cancelled node
                        else
                            // Timeout, but the header node is empty, or the header node has not been cancelled, it returns empty
                            return null;
                    } else if (casHead(h, s = snode(s, e, h, mode))) {
                        // Update the header node to the current node
                        // Then block and wait for matching operation
                        SNode m = awaitFulfill(s, timed, nanos);
                        // If the returned m is a header node, it means that it is cancelled and null is returned
                        if (m == s) {               // wait was cancelled
                            clean(s);
                            return null;
                        }
                        // If the head node is not empty and the next node is s 
                        if ((h = head) != null && h.next == s)
                            casHead(h, s.next);     // help s's fulfiller
                        // Return the item that matches successfully
                        return (E) ((mode == REQUEST) ? m.item : s.item);
                    }
                } else if (!isFulfilling(h.mode)) { // try to fulfill no matching in progress
                    // Check whether the header node is cancelled
                    if (h.isCancelled())            // already cancelled
                        casHead(h, h.next);         // pop and retry
                    // Set the current node as the head node
                    else if (casHead(h, s=snode(s, e, h, FULFILLING|mode))) {
                        // Wait for the match to succeed
                        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;
                            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
                    // Matching
                    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
                    }
                }
            }
        }

The code is complex. Try to write various branches:

  1. The stack is empty, or the first element of the stack is consistent with the current type, either both consumers or producers
    1. If timeout occurs:
      1. If the first element of the stack has been cancelled, update the first element of the stack and spin again
      2. If the element at the beginning of the stack is not cancelled or empty, it directly returns null. To end
    2. There is no timeout. Put the current node at the top of the stack successfully. Wait for matching
      1. Matching failed, timeout, null returned.
      2. If the matching is successful, the corresponding element is returned
  2. There are no matches in progress
    1. If the first element of the stack is cancelled, pop it up and replace it with its next to continue the loop
    2. Replace the first element of the stack with the current element, and the status is matching, successful
      1. Spin and wait for matching. The matching is returned successfully, and the matching continues if it fails
    3. Update failed, continue cycle
  3. Matching is in progress to help update the stack header and next pointer
TransferQueue queue

The first is the node in the queue, which saves the pointer to a node, the elements of the current node, and the waiting thread

//Nodes in queue
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
    final boolean isData;
}

Its properties are:

transient volatile QNode head;
transient volatile QNode tail;
transient volatile QNode cleanMe;

Head and tail.

        @SuppressWarnings("unchecked")
        E transfer(E e, boolean timed, long nanos) {
            QNode s = null; // constructed/reused as needed
            boolean isData = (e != null);

            for (;;) {
                QNode t = tail;
                QNode h = head;
                if (t == null || h == null)         // saw uninitialized value
                    continue;                       // spin

                if (h == t || t.isData == isData) { // empty or same-mode
                    QNode tn = t.next;
                    if (t != tail)                  // inconsistent read
                        continue;
                    if (tn != null) {               // lagging tail
                        advanceTail(t, tn);
                        continue;
                    }
                    if (timed && nanos <= 0L)       // can't wait
                        return null;
                    if (s == null)
                        s = new QNode(e, isData);
                    if (!t.casNext(null, s))        // failed to link in
                        continue;

                    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 {                            // complementary-mode
                    QNode m = h.next;               // node to fulfill
                    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);              // successfully fulfilled
                    LockSupport.unpark(m.waiter);
                    return (x != null) ? (E)x : e;
                }
            }
        }

The queue matching operation is as above

Still spin:

  1. If the queue is empty, spin
  2. If the queue is empty or all nodes are of the same type
    1. If the tail changes, spin again
    2. If the tail is extended backward, spin again
    3. If the timeout occurs, null is returned.
    4. If the current node is empty, create the current node
    5. If you fail to set the current node as the tail of the queue, spin again
    6. Wait for matching. If matching fails, null is returned.
    7. If the matching is successful, the corresponding element will be returned
  3. If the queue is not empty and is not a node of the same type
    1. If the match is successful, the head node will leave the queue and wake up the waiting thread

What is the difference between the two implementation classes? It is used to achieve fairness

Construction method

public SynchronousQueue() {
        this(false);
}

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

If it is fair, the FIFO queue is used. If it is not fair, the stack is used

Team entry method

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

Directly call the transfer method of the transferor, return if successful, or throw an exception

Others are similar

Out of team method

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

Directly call the transfer method of the transferor, return if successful, or throw an exception

Others are similar

WaitQueue wait queue

    @SuppressWarnings("serial")
    static class WaitQueue implements java.io.Serializable { }
    static class LifoWaitQueue extends WaitQueue {
        private static final long serialVersionUID = -3633113410248163686L;
    }
    static class FifoWaitQueue extends WaitQueue {
        private static final long serialVersionUID = -3623113410248163686L;
    }
    private WaitQueue waitingProducers;
    private WaitQueue waitingConsumers;

Wait queue and producer consumer queue are just meaningless empty classes added in JDK 1.5 for serialization

summary

SynchronousQueue implements a queue and a stack according to whether it is fair or not, which is used to save the producers and consumers of the current request

Abstract producers and consumers into nodes in the queue or stack. After each request comes, find another type of node to match. If the match is successful, both nodes will be out of the queue. If the match fails, keep trying

Reference articles

End.

Contact me

Finally, welcome to my personal official account, Yan Yan ten, which will update many learning notes from the backend engineers. I also welcome direct official account or personal mail or email to contact me.

The above are all personal thoughts. If there are any mistakes, please correct them in the comment area.

Welcome to reprint, please sign and keep the original link.

Contact email: huyanshi2580@gmail.com

For more study notes, see personal blog or WeChat official account, Yan Yan ten > > Huyan ten

Posted by phu on Wed, 10 Nov 2021 20:18:10 -0800