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:
- The stack is empty, or the first element of the stack is consistent with the current type, either both consumers or producers
- If timeout occurs:
- If the first element of the stack has been cancelled, update the first element of the stack and spin again
- If the element at the beginning of the stack is not cancelled or empty, it directly returns null. To end
- There is no timeout. Put the current node at the top of the stack successfully. Wait for matching
- Matching failed, timeout, null returned.
- If the matching is successful, the corresponding element is returned
- If timeout occurs:
- There are no matches in progress
- If the first element of the stack is cancelled, pop it up and replace it with its next to continue the loop
- Replace the first element of the stack with the current element, and the status is matching, successful
- Spin and wait for matching. The matching is returned successfully, and the matching continues if it fails
- Update failed, continue cycle
- 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:
- If the queue is empty, spin
- If the queue is empty or all nodes are of the same type
- If the tail changes, spin again
- If the tail is extended backward, spin again
- If the timeout occurs, null is returned.
- If the current node is empty, create the current node
- If you fail to set the current node as the tail of the queue, spin again
- Wait for matching. If matching fails, null is returned.
- If the matching is successful, the corresponding element will be returned
- If the queue is not empty and is not a node of the same type
- 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