Multi-threaded high-concurrency programming (6) -- Semaphore, Exchanger source code analysis

One.Semaphore

  1. Concept

  A counting semaphore. Conceptually, a semaphore maintains a set of permits. Each will block, if necessary acquire(), until a license is available, before it can be used. Each release()add permits, potentially freeing the blocking acquirer. However, the actual license object is not used; Semaphoreonly a count of the available quantity is kept, and enforced accordingly. That is, a Semaphore maintains a set of permits [licenses]. Each call to the acquire() method will block until a license is acquired. Each call to the release() method adds a permit, that is, releases a blocked acquirer. But in fact, this license does not exist. Semaphore only records the number of available resources and makes corresponding behaviors (obtain resources if there are resources, block if there are no resources).

  Semaphores are usually used to limit the number of threads, not to access some (physical or logical) resource.

  Application scenario: control the flow of the system, the thread that gets the semaphore can enter, otherwise it waits. Acquire and release access permissions through acquire() and release().

  • The thread pool controls the number of threads, while the semaphore controls the number of concurrency. Although they look the same, there are still differences between the two.

  • The semaphore is similar to the lock mechanism. When the number of semaphore calls is reached, the thread still exists, but it is suspended. In the thread pool, the number of threads executing at the same time is fixed, and those that exceed the number can only wait.

  Before acquiring an item, each thread must acquire a license from the semaphore to ensure that an item is available. When the thread is done with the item, it returns to the pool and returns the permit to the semaphore, allowing another thread to acquire the item. Note that the synchronization lock is not held when acquire() is called, as this would prevent an item from being returned to the pool. A semaphore encapsulates the synchronization needed to restrict access to the pool, separate from any synchronization needed to keep the pool itself consistent. [ Separate the locks required to restrict access to the pool from the operations on the data in the pool ].

  The semaphore is initialized to one, and is used such that it only allows at most one to be available, which can be used as a mutex. This is often called a binary semaphore because it has only two states: one permit is available, or zero permits are available . When used in this way, a binary semaphore has the property (unlike many Lock implementations) that the "lock" can be released by a thread other than the owner (since semaphores have no notion of ownership). This is useful in some specialized contexts, such as deadlock recovery.

Semaphore(int permits) Creates a Semaphore with the given number of permits and non-fair fairness settings.  
Semaphore(int permits, boolean fair) Create a Semaphore with the given number of permits and the given fairness settings.

  The constructor of this class optionally accepts a fairness parameter. When set to false, this class makes no guarantees about the order in which threads acquire permits. In particular, intrusion is allowed, that is, a thread calling acquire() can advance a permit allocated by an already waiting thread - the logical new thread at the head of the queue of waiting threads puts itself Front of the thread queue]. When fair is set to true, the semaphore guarantees that threads calling the acquire method are selected to obtain permission in the order in which they called these methods (first in, first out; FIFO) [FIFO order refers to the time to reach the execution point inside the method , not the time of method execution. 】. Note that FIFO ordering must apply to specific internal execution points within these methods. Thus, one thread can call acquire before another thread, but arrive at the ordering point after the other thread, and similarly return from the method. Also note that the undefined tryAcquire method does not honor fairness settings, but will take any available license. [ The untimed tryAcquire() method will arbitrarily select available licenses. 】【Unfair locks can be queued for execution, and fair locks are executed in thread order.

  In general, semaphores used to control resource access should be initialized fairly to ensure that no thread is accessing the resource [ensure that no thread starves to death because it cannot obtain a license for a long time ]. When using semaphores for other types of synchronization control, the throughput advantages of non-normal ordering often outweigh fairness.

  2. Usage

A thread can obtain a license  through the acquire() method, and then operate on shared resources. If the license set has been allocated, the thread will enter a waiting state until other threads release the license before they have a chance to acquire a license again. The thread releases a license through The release() method completes and the "permission" will be returned to the Semaphore.

    public static void main(String[] args) {
        ExecutorService service = Executors.newCachedThreadPool();
        final Semaphore sp = new Semaphore(3);
        for (int i = 0; i < 7; i++) {
            Runnable runnable = () -> {
                try {
                    sp.acquire();
                } catch (InterruptedException e1) {
                    e1.printStackTrace();
                }
                System.out.println("线程" + Thread.currentThread().getName() +
                        "Enter, there are currently" + (3 - sp.availablePermits()) + "concurrent");
                try {
                    Thread.sleep((long) (Math.random() * 10000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程" + Thread.currentThread().getName() +
                        "Leaving soon");
                sp.release();
                //The following code is sometimes inaccurately executed, because it does not synthesize atomic units with the above code
                System.out.println("线程" + Thread.currentThread().getName() +
                        "has left, currently has" + (3 - sp.availablePermits()) + "concurrent");
            };
            service.execute(runnable);
        }
    }
result:
Thread pool-1-thread-1 enters, there is currently 1 concurrent
Thread pool-1-thread-2 enters, currently there are 2 concurrent
Thread pool-1-thread-3 enters, currently there are 3 concurrent
Thread pool-1-thread-3 is about to leave
Thread pool-1-thread-4 enters, currently there are 3 concurrent
Thread pool-1-thread-3 has left, there are currently 3 concurrent
Thread pool-1-thread-1 is about to leave
Thread pool-1-thread-1 has left, there are currently 2 concurrent
Thread pool-1-thread-5 enters, currently there are 3 concurrent
Thread pool-1-thread-5 is about to leave
Thread pool-1-thread-5 has left, currently there are 2 concurrent
Thread pool-1-thread-6 enters, currently there are 3 concurrent
Thread pool-1-thread-4 is about to leave
Thread pool-1-thread-4 has left, there are currently 2 concurrent
Thread pool-1-thread-7 enters, currently there are 3 concurrent
Thread pool-1-thread-2 is about to leave
Thread pool-1-thread-2 has left, currently there are 2 concurrent
Thread pool-1-thread-7 is about to leave
Thread pool-1-thread-7 has left, there is currently 1 concurrent
Thread pool-1-thread-6 is about to leave
Thread pool-1-thread-6 has left, there are currently 0 concurrency

  3. acquire analysis

acquire() acquires a permit from this semaphore, blocking until available, or the thread is interrupted.  
void acquire(int permits) Acquires the given number of permits from this semaphore, blocking until all are available, otherwise the thread is interrupted.

    public void acquire() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);//call acquireSharedInterruptibly of AQS
    }
        /**
         * AQS的acquireSharedInterruptibly
         * Acquires in shared mode, aborting if interrupted.  Implemented
         * by first checking interrupt status, then invoking at least once
         * {@link #tryAcquireShared}, returning on success.  Otherwise the
         * thread is queued, possibly repeatedly blocking and unblocking,
         * invoking {@link #tryAcquireShared} until success or the thread
         * is interrupted.
         * Acquired in shared mode, if the interrupt was aborted.
         * The implementation first checks the interrupt status, then calls tryacquired at least once, and returns successfully.
         * Otherwise, the thread is queued and may repeatedly block and unblock,
         * Call tryacquiremred until success or the thread is interrupted.
         */
    public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())//Interrupt throws an exception
            throw new InterruptedException();
        if (tryAcquireShared(arg) < 0)//acquisition failed, join the synchronization queue and wait
            doAcquireSharedInterruptibly(arg);
    }
    //Implemented by Semaphore's FairSync or NonfairSync, resources can be occupied by multiple thread notifications in shared mode, and tryAcquireShared returns an int type, indicating how many resources can be occupied at the same time, which is used to propagate and wake up in shared mode.
    protected int tryAcquireShared(int arg) {
        throw new UnsupportedOperationException();
    }
    //Acquire in shared interrupt mode
    private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        final Node node = addWaiter(Node.SHARED);//Create the node of the current thread, and the lock model is a shared lock, add it to the end of the AQS CLH queue
        boolean failed = true;
        try {
            for (;;) {//spin
                final Node p = node.predecessor();//Get the predecessor node of the current node
                if (p == head) {//is the head node, no waiting node
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {//Acquisition is successful, the current node is set as the head node and propagated [Propagation refers to the remaining permission value of the synchronization state is not 0, and the subsequent nodes are notified to continue to obtain the synchronization state]
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
                //The predecessor node is not a head node, and there is no resource acquisition. Set the status of the predecessor node to SIGNAL, and suspend the thread of the node node through park
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);//The request to end the node thread
        }
    }

  public void acquire(int permits) throws InterruptedException {
        if (permits < 0) throw new IllegalArgumentException();//throw an exception if the number is less than 0
        sync.acquireSharedInterruptibly(permits);//调用AQS的acquireSharedInterruptibly
    }

  tryAcquireShared:

    static final class FairSync extends Sync {//fair lock acquisition
        protected int tryAcquireShared(int acquires) {
            for (;;) {//spin
                //There is a precursor node, which means that there is a blocked thread in front of the current thread, and the acquisition of the current thread fails. Let the previous node thread acquire and run first [compared with unfair lock acquisition, there are more operations to judge the precursor node]
                if (hasQueuedPredecessors())
                    return -1;
                int available = getState();//The number of licenses available
                int remaining = available - acquires;//the remaining number of licenses
                //If the remaining quantity is less than 0 or the remaining quantity is successfully updated, return the remaining quantity
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }
    }
    static final class NonfairSync extends Sync {//Unfair lock acquisition
        protected int tryAcquireShared(int acquires) {
            return nonfairTryAcquireShared(acquires);//Call nonfairTryAcquireShared of Semaphore's internal class Sync
        }
    }
    final int nonfairTryAcquireShared(int acquires) {
        for (;;) {//spin
            int available = getState();
            int remaining = available - acquires;
            if (remaining < 0 ||
                compareAndSetState(available, remaining))
                return remaining;
        }
    }

  4.release analysis

    public void release() {
        sync.releaseShared(1);//Call releaseShared of AQS
    }
    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {//Release the synchronization status successfully
            doReleaseShared();//Wake up the thread of the successor node in the synchronization queue
            return true;
        }
        return false;
    }
    protected boolean tryReleaseShared(int arg) {//Implemented by Semaphore's Sync
        throw new UnsupportedOperationException();
    }
    private void doReleaseShared() {
        /*
         * Ensure that a release propagates, even if there are other
         * in-progress acquires/releases.  This proceeds in the usual
         * way of trying to unparkSuccessor of head if it needs
         * signal. But if it does not, status is set to PROPAGATE to
         * ensure that upon release, propagation continues.
         * Additionally, we must loop in case a new node is added
         * while we are doing this. Also, unlike other uses of
         * unparkSuccessor, we need to know if CAS to reset status
         * fails, if so rechecking.
         * Guaranteed delivery of the release action (to the tail of the synchronous wait queue), even if there are no other ongoing
         * Request or release action. If the successor node of the head node needs to wake up, then perform the wakeup
         * action; if not required, set the wait state of the head node to PROPAGATE guarantee
         * Wake up delivery. In addition, in order to prevent new nodes from entering (queue) during the process, here must
         * Need to do a loop, so it is different from other unparkSuccessor methods
         * What is, if the (head node) wait state setting fails, re-check.
         */
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;//head node status
                 // If the thread corresponding to the head node is in the SIGNAL state, it means the head
                 //The thread corresponding to the successor node of the node needs to be awakened by unpark-.
                if (ws == Node.SIGNAL) {
                    // Modify the thread state corresponding to the head node and set it to 0. If it fails, continue the loop.
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    // Wake up the thread corresponding to the successor node of the head node h
                    unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            // If the head node changes, continue the loop. Otherwise, exit the loop.
            if (h == head)                   // loop if head changed
                break;
        }
    }
    //Wake up the thread corresponding to the successor node of the incoming node
    private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
          if (ws < 0)
              compareAndSetWaitStatus(node, ws, 0);
           // get successor node
          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)
              // wake up the thread
              LockSupport.unpark(s.thread);
    }
    protected final boolean tryReleaseShared(int releases) {
        for (;;) {//spin
            int current = getState();//Get the current synchronization state
            int next = current + releases;//status +1
            if (next < current) // overflow
                throw new Error("Maximum permit count exceeded");
            if (compareAndSetState(current, next))//Update status successfully returns true
                return true;
        }
    }

Two. Exchanger

  1. Concept

  A synchronization point at which threads can pair and exchange elements within a pair. Each thread provides some objects when it enters the exchange method, matches with a partner thread, and receives its partner's objects on return. Exchanges can be thought of as a bidirectional form of SynchronousQueue. Exchangers may be useful in applications such as genetic algorithms and pipeline design.

  An encapsulation tool class for exchanging data between two working threads. Simply put, one thread wants to exchange data with another thread after completing a certain transaction. The first thread that takes out the data first will wait for the second Threads cannot exchange corresponding data with each other until the second thread arrives with the data.

 

  2. Usage

  Exchanger<V> generic type, where V represents an exchangeable data type

  • V exchange(V v): Waits for another thread to reach this exchange point (unless the current thread is interrupted), then transfers the given object to this thread, and receives this thread's object.

  • V exchange(V v, long timeout, TimeUnit unit): waits for another thread to reach this exchange point (unless the current thread is interrupted or exceeds the specified wait time), then transfers the given object to this thread, and receives the the thread object.

        Exchanger<Integer> exchanger = new Exchanger<>();
        ExecutorService executor = Executors.newCachedThreadPool();
        Runnable run = () ->{
            try {
                int num = new Random().nextInt(10);
                System.out.println(Thread.currentThread().getName()+"Start exchanging data:"+num);
                num = exchanger.exchange(num);//Exchange data and get the exchanged data
                System.out.println(Thread.currentThread().getName()+"Data after data exchange: "+num);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };
        executor.execute(run);
        executor.execute(run);
        executor.shutdown();
result:
pool-1-thread-2 starts exchanging data: 9
pool-1-thread-1 starts exchanging data: 8
Data after pool-1-thread-2 exchange data: 8
Data after pool-1-thread-1 exchange data: 9

Guess you like

Origin blog.csdn.net/weixin_45536242/article/details/125741270