Introduction to Java Synchronizer

Keywords: Java JDK

Catalog

Synchronizer

Synchronizer provides a series of functions such as thread synchronization, mutually exclusion and waiting for multiple threads to access and modify the same resource in a multi-threaded environment. The main synchronizer classes provided by JDK inherit as follows:

Introduction to AbstractOwnable Synchronizer Class

The basic function of the AbstractOwnableSynchronizer class is to be exclusive to a thread (the meaning of the class name). AbstractOwnable Synchronizer has a private property exclusive Owner Thread, which is a reference to an exclusive thread and provides the get, set method for that thread.

AbstractQueuedSynchronizer

AbstractQueued Synchronizer is an important implementation of synchronizer. AbstractQueued Synchronizer is a multi-threaded synchronization tool based on FIFO waiting queues. Threads on the AbstractQueued Synchronizer wait queue will have a corresponding int value that requires atomic operations to indicate the current state of the thread. AbstractQueued Synchronizer consists of two parts, one is to require subclasses to implement state-changing methods, that is, to implement the logic of acquiring and releasing locks; the other is to realize the functions of thread schedule waiting queue spin and blocking, notification wake-up, waiting queue and so on.

AbstractQueued Synchronizer itself provides a large number of public methods, and the method implements the function of thread synchronization, such as thread entering the waiting queue, which is not necessarily suitable for direct exposure to other classes, so the subclass is required to be an internal class of non-public type.

AbstractQueued Synchronizer provides two synchronization strategies: exclusive mode and shared mode. Exclusive mode allows only one thread to acquire locks. If one thread has acquired the lock at present, all other threads enter the waiting queue when they acquire the lock. Shared mode allows multiple threads to acquire locks at the same time, but it does not guarantee that threads will succeed in acquiring locks. AbstractQueued Synchronizer itself is not a concrete implementation of acquiring and releasing locks, but in order to take into account that in the case of steganography, subclasses may only need to provide a policy pattern, so the method defined for acquiring and releasing locks is protected, and the method body only throws an Unsupported OperationException exception. But AbstractQueued Synchronizer itself is responsible for maintaining the waiting queue and notification wake-up, so once a thread acquires a lock in shared mode, AbstractQueued Synchronizer needs to determine whether the next waiting thread needs to acquire a lock as well. AbstractQueued Synchronizer maintains only one bidirectional list as a waiting queue, so when different threads use different synchronization strategies, they are all on a waiting queue. If only one synchronization strategy is needed for subclasses, then only one synchronization strategy is needed.

Locking defines the interface for acquiring race conditions, and locks generally rely on synchronizers to implement many functions, possibly to facilitate the implementation of locks, so in AbstractQueued Synchronizer there is an internal class of the race Condition implementation class ConditionObject.

AbstractQueued Synchronizer requires that subclasses implement

  • tryAcquire Gets Exclusive Locks
  • tryRelease releases exclusive lock
  • tryAcquireShared Gets Shared Locks
  • tryReleaseShared Releases Shared Shared Locks
  • Is isHeldExclusively the exclusive lock of the current thread holding the synchronizer?

When using the above method, thread safety must be ensured, and the execution time of this method should be as short as possible, and it should not be blocked.

Threads are dropped into the waiting queue by AbstractQueued Synchronizer, which is not an atomic operation. The core logic to successfully enter the waiting queue is as follows:

while (!tryAcquire(arg)) {
    //Enter the queue if not already in the queue
    //Blocking the current thread as needed
}

while (!tryRelease(arg)) {
    //The thread that binds the first element of the queue wakes up LockSupport.unpack
}

Since many judgment checks are handled before queuing, if no special processing is done, subsequent incoming threads (entering the waiting queue) may enter the waiting queue earlier than the advanced threads. The way to do no special processing is unfair lock, and it is fair lock that guarantees that the advanced thread will enter the queue first and wait in the queue.

AbstractQueued Synchronizer provides a basic but funny and extensible synchronizer function, but AbstractQueued Synchronizer is not a complete synchronizer. Some functions of AbstractQueued Synchronizer, such as tryAcquire, are implemented by dependent subclasses.

The following is an example of a complete synchronizer in a document:

 class Mutex implements Lock, java.io.Serializable {

  // Our internal helper class
  private static class Sync extends AbstractQueuedSynchronizer {
    // Report whether in locked state
    protected boolean isHeldExclusively() {
      return getState() == 1;
    }

    // Acquire the lock if state is zero
    public boolean tryAcquire(int acquires) {
      assert acquires == 1; // Otherwise unused
      if (compareAndSetState(0, 1)) {
        setExclusiveOwnerThread(Thread.currentThread());
        return true;
      }
      return false;
    }

    // Release the lock by setting state to zero
    protected boolean tryRelease(int releases) {
      assert releases == 1; // Otherwise unused
      if (getState() == 0) throw new IllegalMonitorStateException();
      setExclusiveOwnerThread(null);
      setState(0);
      return true;
    }

    // Provide a Condition
    Condition newCondition() { return new ConditionObject(); }

    // Deserialize properly
    private void readObject(ObjectInputStream s)
        throws IOException, ClassNotFoundException {
      s.defaultReadObject();
      setState(0); // reset to unlocked state
    }
  }

  // The sync object does all the hard work. We just forward to it.
  private final Sync sync = new Sync();

  public void lock()                { sync.acquire(1); }
  public boolean tryLock()          { return sync.tryAcquire(1); }
  public void unlock()              { sync.release(1); }
  public Condition newCondition()   { return sync.newCondition(); }
  public boolean isLocked()         { return sync.isHeldExclusively(); }
  public boolean hasQueuedThreads() { return sync.hasQueuedThreads(); }
  public void lockInterruptibly() throws InterruptedException {
    sync.acquireInterruptibly(1);
  }
  public boolean tryLock(long timeout, TimeUnit unit)
      throws InterruptedException {
    return sync.tryAcquireNanos(1, unit.toNanos(timeout));
  }
}
  • Special caution: Mutex cannot directly inherit AbstractQueued Synchronizer, because AbstractQueued Synchronizer has too many public methods related to synchronizer functions to be exposed. So Mutex has an internal class Sync, which inherits AbstractQueued Synchronizer and implements functions not provided by AbstractQueued Synchronizer such as tryAcquire.

AbsctractQueued Synchronizer attributes and methods are introduced:

public abstract class AbstractQueuedSynchronizer
  extends AbstractOwnableSynchronizer
  implements java.io.Serializable {

  private static final long serialVersionUID = 7373984972572414691L;

  /**
   * Create a synchronizer with a synchronization state of 0 (state=0)
   */
  protected AbstractQueuedSynchronizer() { }

  /**
   *
   * Node It is the basic data structure of the synchronizer waiting queue. For more information, please refer to Condition Notes about the Node section.
   *
   */
  static final class Node {...}

  /**
   * Node The first element of a two-way linked list
   * When AbstractQueued Synchronizer is initialized, the head points to null. If you need to initialize AbstractQueued Synchronizer, construct
   * Build the head element, then set the value of the head through the setHead method. And make sure that waitStatus is not a CANCELLED state.
   */
  private transient volatile Node head;

  /**
   * Node The last element of the bidirectional list. The value of tail is changed by enq.
   */
  private transient volatile Node tail;

  /**
   * Status of Synchronizer
   */
  private volatile int state;

  /**
   * Returns the status of the synchronizer
   */
  protected final int getState() {
      return state;
  }

  /**
   * Change the state of synchronizer
   */
  protected final void setState(int newState) {
      state = newState;
  }

  /**
   * Compare and modify the state of synchronizer. If it is equal to the given value, the state is changed to a specific value.
   *
   */
  protected final boolean compareAndSetState(int expect, int update) {
      // See below for intrinsics setup to support this
      return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
  }

  /**
   * Threshold of spin time.
   * Before blocking threads, threads try to acquire locks until they exceed this threshold, using the LockSupport.park method to block threads.
   * Thread blocking and wake-up require a certain amount of resource overhead, which can be saved if locks are acquired during spinning.
   * But spin represents that threads use computer resources to do things beyond their goals. Although waking up threads requires resource overhead, spinning always wastes current CPU resources.
   * So spin time is not recommended very long.
   */
  static final long spinForTimeoutThreshold = 1000L;

  /**
   * Add the Node element to the list. This is a FIFO queue, so all the elements added are added to the end of the queue.
   */
  private Node enq(final Node node) {
      for (;;) {
          Node t = tail;
          if (t == null) { // Must initialize
              if (compareAndSetHead(new Node()))
                  tail = head;
          } else {
              node.prev = t;
              if (compareAndSetTail(t, node)) {
                  t.next = node;
                  return t;
              }
          }
      }
  }

  /**
   * Create an element based on the given policy and associated with the current thread, and queue the element.
   */
  private Node addWaiter(Node mode) {
      Node node = new Node(Thread.currentThread(), mode);
      // Try the fast path of enq; backup to full enq on failure
      Node pred = tail;
      if (pred != null) {
          node.prev = pred;
          if (compareAndSetTail(pred, node)) {
              pred.next = node;
              return node;
          }
      }
      enq(node);
      return node;
  }

  /**
   * Set the first element of the queue and empty the relevant thread of the element and the previous element. The first purpose of vacancy is to remind GC to recycle.
   * The second is to abolish unnecessary queue-related operations.
   *
   * @param node the node
   */
  private void setHead(Node node) {
      head = node;
      node.thread = null;
      node.prev = null;
  }

  /**
   * Wake up threads bound by subsequent elements.
   *
   * @param node the node
   */
  private void unparkSuccessor(Node node) {
      /*
       * If waitStatus is a negative number (usually indicating that a subsequent element of the list needs to be told that the current element is awakened), change the waitStatus of the element to 0.
       * If the current operation is not modified successfully, or if the thread bound by subsequent elements modifies the value of waitStatus, there will be no functional impact on the synchronizer.
       */
      int ws = node.waitStatus;
      if (ws < 0)
          compareAndSetWaitStatus(node, ws, 0);

      /*
       * Wake up threads bound by subsequent elements. If the next element specified by the current element does not exist, a waitStatus <= 0 element is traced back from the end of the list.
       * Wake up the thread that binds it
       */
      Node s = node.next;
      if (s == null || s.waitStatus > 0) {
          s = null;
          for (Node t = tail; t != null && t != node; t = t.prev)
              if (t.waitStatus <= 0)
                  s = t;
      }
      if (s != null)
          LockSupport.unpark(s.thread);
  }

  /**
   * Release locks in shared mode.
   */
  private void doReleaseShared() {
      /*
       * You need to ensure that the head state is set to PROPAGATE
       */
      for (;;) {
          Node h = head;
          if (h != null && h != tail) {
              int ws = h.waitStatus;
              if (ws == Node.SIGNAL) {
                  /**
                   * Set the state to 0. If the setup fails, it means that other threads have set the state to another state, then the check is directly recycled.
                   */
                  if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                      continue;            // loop to recheck cases
                  /**
                   * If the status setting is successful, wake up the subsequent elements
                   */
                  unparkSuccessor(h);
              }
              else if (ws == 0 &&
                       !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                  /**
                   * ws == 0 This means that the state of the first element has been set to zero by other threads, and that subsequent elements have been notified to wake up or have no subsequent elements.
                   * If the setup fails, the check is repeated.
                   */
                  continue;                // loop on failed CAS
          }
          if (h == head)                   // loop if head changed
              break;
      }
  }

  /**
   * Exit the first element of the original queue and set the specified element to the first element of the queue
   *
   * @param node the node
   * @param propagate the return value from a tryAcquireShared
   */
  private void setHeadAndPropagate(Node node, int propagate) {
      Node h = head; // Record old head for check below
      setHead(node);
      /*
       * If the result of tryAcquireShared is greater than 0 (indicating that the shared lock is held by other threads), or if the first element of the queue has been marked empty by other threads,
       * Or when the status of the first element in the queue is propagated or notified, subsequent elements need to be awakened.
       *
       */
      if (propagate > 0 || h == null || h.waitStatus < 0 ||
          (h = head) == null || h.waitStatus < 0) {
          Node s = node.next;
          if (s == null || s.isShared())
              doReleaseShared();
      }
  }

  /**
   * Unlock acquisition
   *
   * @param node the node
   */
  private void cancelAcquire(Node node) {
      if (node == null)
          return;

      node.thread = null;

      //Filter out elements that have been cancelled before the current element
      Node pred = node.prev;
      while (pred.waitStatus > 0)
          node.prev = pred = pred.prev;

      //Subsequent CAS modifications need to be used
      Node predNext = pred.next;

      //Set the status of the current element to cancel and prepare for subsequent exit queues.
      node.waitStatus = Node.CANCELLED;

      if (node == tail && compareAndSetTail(node, pred)) {
          //If the current element is at the end of the queue, the queue is cleared from the waiting queue and the last element of the element is set at the end of the queue.
          compareAndSetNext(pred, predNext, null);
      } else {
          // If subsequent elements need to be waked up, the elements are queued (resetting the values of the upper and lower element pointers of the previous element and the next element)
          //And set the state of an element on a meta to SIGNAL or wake up subsequent elements
          int ws;
          if (pred != head &&
              ((ws = pred.waitStatus) == Node.SIGNAL ||
               (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
              pred.thread != null) {
              //Only the head or thread bound by cancel elements is null
              Node next = node.next;
              if (next != null && next.waitStatus <= 0)
                  //If the element has subsequent elements, then the element is queued.
                  compareAndSetNext(pred, predNext, next);
          } else {
              //Wake up the following elements
              unparkSuccessor(node);
          }

          node.next = node; // help GC
      }
  }

  /**
   * If the node contention lock fails, the state of the waiting queue is checked and the value of the state is modified to SIGNAL if the condition is met.
   * If you decide that the thread should be blocked, return true; otherwise return false.
   * Only the former element is the first element of the queue can compete for locks. But the second element of the queue does not necessarily determine the competition to the lock.
   *
   * @param pred node's predecessor holding status
   * @param node the node
   * @return {@code true} if thread should block
   */
  private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
      //Status of the previous node of the current node
      int ws = pred.waitStatus;
      if (ws == Node.SIGNAL)
          /*
           * If the state of the previous element is SIGNAL indicating that it can be safely suspended, return true directly.
           */
          return true;
      if (ws > 0) {
          /*
           * Remove the cancelled element from the queue.
           */
          do {
              node.prev = pred = pred.prev;
          } while (pred.waitStatus > 0);
          //node.prev has been updated in the loop
          pred.next = node;
      } else {
          /*
           * The state of the waiting queue should be 0 (initialized state) or PROPAGATE (propagation), which means waiting for wake-up notification before the money is blocked.
           * The thread invoking the method needs to ensure that the lock cannot be acquired before blocking.
           */
          compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
      }
      return false;
  }

  /**
   * Block the current thread and return the thread interrupt flag.
   *
   * @return {@code true} if interrupted
   */
  private final boolean parkAndCheckInterrupt() {
      LockSupport.park(this);
      return Thread.interrupted();
  }

  /**
   * Get locks in exclusive mode. This method does not respond to thread interruption, but it returns the thread interruption flag.
   *
   * @param node the node
   * @param arg the acquire argument
   * @return {@code true} if interrupted while waiting
   */
  final boolean acquireQueued(final Node node, int arg) {
      boolean failed = true;
      try {
          boolean interrupted = false;
          //
          for (;;) {
              final Node p = node.predecessor();
              //Locks are acquired only when the previous element is the first element in the queue.
              //In the case of an unfair lock, although it is the second element of the queue, it is not necessarily possible to acquire the lock. If there is an external method call during the acquisition process to obtain the lock successfully, then the current thread still needs to continue hanging.
              if (p == head && tryAcquire(arg)) {
                  setHead(node);
                  p.next = null; // help GC
                  failed = false;
                  return interrupted;
              }

              if (shouldParkAfterFailedAcquire(p, node) &&
                  parkAndCheckInterrupt())
                  //The thread is interrupted and the interrupt flag is set to true. But because the process in the for loop is spinned back immediately, thread interruption does not have an impact.
                  interrupted = true;
          }
      } finally {
          //If the lock is acquired incorrectly, the current element is queued
          if (failed)
              cancelAcquire(node);
      }
  }

  /**
   * Acquisition locks that respond to interruptions
   * @param arg the acquire argument
   */
  private void doAcquireInterruptibly(int arg)
      throws InterruptedException {
      final Node node = addWaiter(Node.EXCLUSIVE);
      boolean failed = true;
      try {
          for (;;) {
              final Node p = node.predecessor();
              if (p == head && tryAcquire(arg)) {
                  setHead(node);
                  p.next = null; // help GC
                  failed = false;
                  return;
              }
              if (shouldParkAfterFailedAcquire(p, node) &&
                  parkAndCheckInterrupt())
                  //LockSuport.park can respond to interrupts. Once the thread interrupts and the thread recovers from the blocking, an exception is thrown.
                  throw new InterruptedException();
          }
      } finally {
          if (failed)
              cancelAcquire(node);
      }
  }

  /**
   * On the basis of responding to interrupt acquisition lock, add the function of waiting timeout
   *
   * @param arg the acquire argument
   * @param nanosTimeout max wait time
   * @return {@code true} if acquired
   */
  private boolean doAcquireNanos(int arg, long nanosTimeout)
          throws InterruptedException {
      if (nanosTimeout <= 0L)
          return false;
      final long deadline = System.nanoTime() + nanosTimeout;
      final Node node = addWaiter(Node.EXCLUSIVE);
      boolean failed = true;
      try {
          for (;;) {
              final Node p = node.predecessor();
              if (p == head && tryAcquire(arg)) {
                  setHead(node);
                  p.next = null; // help GC
                  failed = false;
                  return true;
              }
              nanosTimeout = deadline - System.nanoTime();
              if (nanosTimeout <= 0L)
                  return false;
              //Spin until the time threshold of spin is not exceeded.
              if (shouldParkAfterFailedAcquire(p, node) &&
                  nanosTimeout > spinForTimeoutThreshold)
                  LockSupport.parkNanos(this, nanosTimeout);
              if (Thread.interrupted())
                  throw new InterruptedException();
          }
      } finally {
          if (failed)
              cancelAcquire(node);
      }
  }

  /**
   * Acquisition of locks without interruption in shared mode
   * In exclusive mode, if there is a thread from acquisition to lock start to release the lock end, the whole waiting queue only spins to try to acquire the lock for the queue entry status and queue entry status are waiting to acquire the lock status.
   * If a lock is acquired in shared mode, it checks whether subsequent elements are shared locks, and if they are waked up directly.
   * @param arg the acquire argument
   */
  private void doAcquireShared(int arg) {
      final Node node = addWaiter(Node.SHARED);
      boolean failed = true;
      try {
          boolean interrupted = false;
          for (;;) {
              final Node p = node.predecessor();
              if (p == head) {
                  int r = tryAcquireShared(arg);
                  if (r >= 0) {
                      setHeadAndPropagate(node, r);
                      p.next = null; // help GC
                      if (interrupted)
                          selfInterrupt();
                      failed = false;
                      return;
                  }
              }
              if (shouldParkAfterFailedAcquire(p, node) &&
                  parkAndCheckInterrupt())
                  interrupted = true;
          }
      } finally {
          if (failed)
              cancelAcquire(node);
      }
  }

  /**
   * In exclusive mode, the lock is acquired without responding to interrupts.
   * First try to acquire the lock (tryAcquire - concrete subclass implementation), and if you fail to add elements to the queue (addWaiter),
   * Then determine whether the newly created element can hold the acquireQueued.
   * If the thread is interrupted, the current thread is set to the selfInterrupt state.
   *
   * @param arg the acquire argument.  This value is conveyed to
   *        {@link #tryAcquire} but is otherwise uninterpreted and
   *        can represent anything you like.
   */
  public final void acquire(int arg) {
      if (!tryAcquire(arg) &&
          acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
          selfInterrupt();
  }

  /**
   * Release locks in exclusive mode.
   * Release exclusive locks (tryRelease - concrete subclass implementation) to wake up subsequent elements in the queue.
   *
   * @param arg the release argument.  This value is conveyed to
   *        {@link #tryRelease} but is otherwise uninterpreted and
   *        can represent anything you like.
   * @return the value returned from {@link #tryRelease}
   */
  public final boolean release(int arg) {
      if (tryRelease(arg)) {
          Node h = head;
          if (h != null && h.waitStatus != 0)
              unparkSuccessor(h);
          return true;
      }
      return false;
  }

  /**
   * Get locks in shared mode.
   * Get the lock in shared mode (tryAcquireShared - concrete subclass implementation), and if the acquisition fails, get it without responding to interrupts
   * Do Acquire Shared.
   *
   * @param arg the acquire argument.  This value is conveyed to
   *        {@link #tryAcquireShared} but is otherwise uninterpreted
   *        and can represent anything you like.
   */
  public final void acquireShared(int arg) {
      if (tryAcquireShared(arg) < 0)
          doAcquireShared(arg);
  }

  /**
   * Release locks in shared mode
   * Attempt to release the lock (try Release Shared), if successful notification of subsequent elements (do Release Shared)
   *
   * @param arg the release argument.  This value is conveyed to
   *        {@link #tryReleaseShared} but is otherwise uninterpreted
   *        and can represent anything you like.
   * @return the value returned from {@link #tryReleaseShared}
   */
  public final boolean releaseShared(int arg) {
      if (tryReleaseShared(arg)) {
          doReleaseShared();
          return true;
      }
      return false;
  }
}

Introduction to the Implementation of AbstractQueued Synchronizer Subclass

There are many subclasses of AbstractQueued Synchronizer, such as ReentrantLock.Sync, ThreadPool Executor. Worker, etc. Here is only a list of ReentrantLock.FairSync classes that implement AbstractQueuedSynchronizer itself without providing implementation details that require subclasses to implement. The implementation instructions for this part of the method are given.

tryAcquire

The code is as follows:

/**
 * Locks are acquired fairly (first come first served) under monopoly strategy
 */
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    //C== 0 means that locks can be acquired
    if (c == 0) {
        //Determine whether there is a precursor element, and if so, the acquisition lock fails. HasQueued Predecessors is a key function to ensure fair locks.
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //Reentry locks are specific and can be held by multiple threads.
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

tryRelease

ReentrantLock.Sync's tryRelease implementation is as follows:

/**
 * Release the lock. Only when all threads release the lock will true be returned, otherwise it will only reduce the value of the state.
 */
protected final boolean tryRelease(int releases) {
     int c = getState() - releases;
     if (Thread.currentThread() != getExclusiveOwnerThread())
         throw new IllegalMonitorStateException();
     boolean free = false;
     if (c == 0) {
         free = true;
         setExclusiveOwnerThread(null);
     }
     setState(c);
     return free;
 }

Posted by nbarone on Wed, 18 Sep 2019 06:59:31 -0700