Research on Davids principle: thread synchronizer in Java Concurrent package (CountDownLatch, CyclicBarrier and Semaphore)

Posted by Richard Bowser on Mon, 15 Jun 2020 06:48:35 +0200

Article catalog

Thread synchronizer in Java Concurrent package (CountDownLatch, CyclicBarrier and Semaphore)

CountDownLatch

Using AQS implementation, the counter value is assigned to the state of AQS through the constructor, and the state field of AQS is used to represent the counter value, which is disposable and cannot be reused.

public CountDownLatch(int count) {
    if (count < 0) throw new IllegalArgumentException("count < 0");
	// Call Syn (class Sync extends AbstractQueuedSynchronizer)
    this.sync = new Sync(count);
}

Sync(int count) {
    setState(count);
}

Core method 1: countDown

Every time the countDown counter is executed, the AQS releaseShared method CAS is called to reduce 1, countDown will not block, only CAS will fail and retry. If it is = = 0 after reduction, the waiting thread will be awakened to process the subsequent operation.

public void countDown() {
	// Call AQS
    sync.releaseShared(1);
}
// CountDownLatch implementation method
protected boolean tryReleaseShared(int releases) {
	// To signal when transitioning to zero
    for (;;) {
        int c = getState();
        if (c == 0)
            return false;
        int nextc = c-1;
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}

Core method 2: await

  1. The await method is blocked until all threads call countDown and state == 0.
  2. It is interrupted by other threads and an exception is thrown.
  3. Timeout return.
// No timeout await
public void await() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
	// Throw an interrupt exception if the thread is interrupted
    if (Thread.interrupted())
        throw new InterruptedException();
	// Try - 1, equivalent to state! = 0, put it in the condition queue to wait, and return directly if it is 0
    if (tryAcquireShared(arg) < 0)
		// AQS Sync method
        doAcquireSharedInterruptibly(arg);
}

// A positive number indicates state == 0
protected int tryAcquireShared(int acquires) {
    return (getState() == 0) ? 1 : -1;
}

// Timeout await
public boolean await(long timeout, TimeUnit unit)
    throws InterruptedException {
    return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}

public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)
        throws InterruptedException {
	// Throw an interrupt exception if the thread is interrupted
    if (Thread.interrupted())
        throw new InterruptedException();
	// If state == 0, it will directly return true, otherwise it will be put into the condition queue and wait for timeout
    return tryAcquireShared(arg) >= 0 ||
		// AQS Sync method
        doAcquireSharedNanos(arg, nanosTimeout);
}

Core method 3: getCount

Get the value of the current counter, that is, get the state value of AQS.

	// Methods of CountDownLatch class
	public long getCount() {
        return sync.getCount();
    }

	// Methods of CountDownLatch class
	int getCount() {
		// AQS Sync method
        return getState();
    }

CyclicBarrier

Loopback barrier: when all threads call the await method, the threads will break through the barrier and continue to run downward, which can be reused (two fields are maintained internally, one party saves the set counter value, and one count calculates the current counter value). For example, after all threads have finished executing task 1, they can execute task 2. After all threads have finished executing task 2, they can execute task 3 and declare a CyclicBarrier.
Example:

private static CyclicBarrier cyclicBarrier = new CyclicBarrier(2);

public static void main( String[] args ) {

    ExecutorService executorService = Executors.newFixedThreadPool(2);
    executorService.submit(new Thread(() -> {
        try {
            System.out.println(Thread.currentThread() + "step 1");
            cyclicBarrier.await();
            System.out.println(Thread.currentThread() + "step 2");
            cyclicBarrier.await();
            System.out.println(Thread.currentThread() + "step 3");
        } catch (InterruptedException | BrokenBarrierException e) {
            e.printStackTrace();
        }
    }));
    executorService.submit(new Thread(() -> {
        try {
            System.out.println(Thread.currentThread() + "step 1");
            cyclicBarrier.await();
            System.out.println(Thread.currentThread() + "step 2");
            cyclicBarrier.await();
            System.out.println(Thread.currentThread() + "step 3");
        } catch (InterruptedException | BrokenBarrierException e) {
            e.printStackTrace();
        }
    }));
    System.out.println("over");
    executorService.shutdown();
}

Core method 1: await

Blocking method, implement atomic update of counter by exclusive lock ReentrantLock, and realize thread synchronization by using conditional variable queue. The following conditions will be returned:

  1. The parties threads called the await method, that is, all threads reached the barrier point (return true).
  2. If another thread calls the interrupt method of the current thread to interrupt the current thread, the current thread will throw an InterruptedException exception and return.
  3. When the token flag of the Generation object associated with the current barrier point is set to true, a broken barrierexception exception is thrown and returned.
  4. Timeout return (return false).

If the counter is 0 after await is called, the thread in the wake-up condition queue will perform subsequent operations through the barrier. If it is not 0 after await, the thread will be placed in the condition queue to wait.

// Non timeout await
public int await() throws InterruptedException, BrokenBarrierException {
    try {
        return dowait(false, 0L);
    } catch (TimeoutException toe) {
        throw new Error(toe); // cannot happen
    }
}

// Timeout await
public int await(long timeout, TimeUnit unit)
        throws InterruptedException,
               BrokenBarrierException,
               TimeoutException {
 return dowait(true, unit.toNanos(timeout));
}

private int dowait(boolean timed, long nanos)
        throws InterruptedException, BrokenBarrierException,
               TimeoutException {
    // Get reentrant exclusive lock
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
    	// Check whether the current barrier is broken. If yes, a BrokenBarrierException will be thrown
        final Generation g = generation;

        if (g.broken)
            throw new BrokenBarrierException();
        if (Thread.interrupted()) {
        	// If the current thread is interrupted:
			// Break the barrier: generation.broken = true;
	        // Reset counter: count = parties;
	        // Wake up all waiting threads: trip.signalAll();
	        // And throw an interrupt exception InterruptedException
            breakBarrier();
            throw new InterruptedException();
        }
		// Execute here to indicate that the status is normal. Start the await operation, and set the current count - 1
		// If = = 0 after - 1, set the barrier to true and reset count
        int index = --count;
        if (index == 0) {
            boolean ranAction = false;
            try {
                final Runnable command = barrierCommand;
                if (command != null)
                	// Perform task
                    command.run();
                ranAction = true;
                // Activate other threads blocked by calling the await method, reset the counter and start the new generation of setting broken = false
                nextGeneration();
                return 0;
            } finally {
                if (!ranAction)
                    breakBarrier();
            }
        }

        // Execution here indicates that you still need to wait until the interrupt, timeout or barrier is broken
        for (;;) {
            try {
            	// No timeout
                if (!timed)
                    trip.await();
                // Include timeout
                else if (nanos > 0L)
                    nanos = trip.awaitNanos(nanos);
            } catch (InterruptedException ie) {
                if (g == generation && ! g.broken) {
                    breakBarrier();
                    throw ie;
                } else {
                    // Even if we are not interrupted, we will complete the waiting, so the interruption is regarded as "belonging" to subsequent execution.
                    Thread.currentThread().interrupt();
                }
            }

            if (g.broken)
                throw new BrokenBarrierException();

            if (g != generation)
                return index;
			// Over time break the barrier
            if (timed && nanos <= 0L) {
                breakBarrier();
                throw new TimeoutException();
            }
        }
    } finally {
        lock.unlock();
    }
}

// Start a new generation
private void nextGeneration() {
  	// Wake up a thread blocked by a call to await
    trip.signalAll();
    // Reset CyclicBarrier counter
    count = parties;
    // Reset Generation, broken == false
    generation = new Generation();
}

Core method 2: reset

public void reset() {
  final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        // Break the barrier
        breakBarrir();
        // Start a new generation
        nextGeneration();
    } finally {
        lock.unlock();
    }
}

// Break the barrier
private void breakBarrier() {
	// Break the barrier
  	generation.broken = true;
  	// Reset counter
    count = parties;
    // Wake up all waiting threads
    trip.signalAll();
}

// Start a new generation
private void nextGeneration() {
	// Wake up all waiting threads
    trip.signalAll();
    // Reset counter
    count = parties;
    // Start a new generation
    generation = new Generation();
}

Semaphore

Semaphore semaphore semaphore is also a synchronizer in Java. Unlike CountDownLatch and CyclicBarrier, its internal counter is increasing. When semaphore is initialized, you can specify an initial value. However, you do not need to know the number of threads that need to be synchronized, but you need to specify the number of threads that need to be synchronized when you call the acquire method where synchronization is needed.

Core method 1: construction

// Default unfairness
public Semaphore(int permits) {
    sync = new NonfairSync(permits);
}

// Can specify whether it is fair
public Semaphore(int permits, boolean fair) {
    sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}

// Internally, AQS is called to set state value, and different Sync is constructed according to fair
Sync(int permits) {
    setState(permits);
}

Core method 2: acquire

// Nonparametric method equals acquire(1)
public void acquire() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

// AQS method
public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
    // Check whether the thread is interrupted, and throw an exception if interrupted
    if (Thread.interrupted())
        throw new InterruptedException();
    // Call Semaphore implementation method tryAcquireShared
    if (tryAcquireShared(arg) < 0)
        doAcquireSharedInterruptibly(arg);
}

// NonfairSync
protected int tryAcquireShared(int acquires) {
    return nonfairTryAcquireShared(acquires);
}

// Unfair lock implementation
final int nonfairTryAcquireShared(int acquires) {
  for (;;) {
  		// Get current signal value
        int available = getState();
        // Compared with the parameters, the number of remaining semaphores is obtained
        int remaining = available - acquires;
        // If it is less than 0, it means that it has not been consumed. It directly returns a negative number and executes doacquireshared interruptible (ARG); it can block interrupt
        // Otherwise, set the status value to remaining and return the remaining value > = 0. Exit the acquire method to continue the subsequent business
        if (remaining < 0 ||
            compareAndSetState(available, remaining))
            return remaining;
    }
}

// FairSync
protected int tryAcquireShared(int acquires) {
    for (;;) {
    	// The difference with unfair lock is to determine whether the current thread is in the blocking queue head
    	// If it is included in the queue, a negative number is returned and doacquireshared interruptible (ARG) is executed; interruptible blocking is performed
        if (hasQueuedPredecessors())
            return -1;
        // Consistent with unfair realization
        int available = getState();
        int remaining = available - acquires;
        if (remaining < 0 ||
            compareAndSetState(available, remaining))
            return remaining;
    }
}

Core method 3: acquire uninterruptible

Similar to the acquire method, the difference is that this method is not affected by the interrupt. After the current thread calls acquirenterruptibly, if other threads call the interrupt method of the current thread and set the interrupt flag of the current thread, the current thread will not throw the InterruptException exception and return.

Core method 4: release

// Default release shared without parameters (1)
public void release() {
    sync.releaseShared(1);
}

public void release(int permits) {
    if (permits < 0) throw new IllegalArgumentException();
    sync.releaseShared(permits);
}

// Try + arg, if successful, then doReleaseShared, otherwise return false directly
public final boolean releaseShared(int arg) {
 if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

// If CAS + release is successful, true will be returned. That is to say, either true or error will be returned. There is no false condition-_ -!!!
protected final boolean tryReleaseShared(int releases) {
   for (;;) {
        int current = getState();
        int next = current + releases;
        if (next < current) // overflow
            throw new Error("Maximum permit count exceeded");
        if (compareAndSetState(current, next))
            return true;
    }
}

private void doReleaseShared() {
    for (;;) {
        Node h = head;
        
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;
                // If CAS succeeds, wake up the first element of the team
                unparkSuccessor(h);
            }
            // Continue to loop if CAS fails
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;
        }
        // Continue the loop if the head changes, otherwise exit the method directly
        if (h == head)
            break;
    }
}

summary

Thread synchronizer is an important class about thread cooperation. First, CountDownLatch provides more flexible control through counters. As long as the counter value is detected as 0, it will continue to execute. Compared with join, the main thread will continue to run down more flexibly only after the thread has finished executing. In addition, CyclicBarrier can also achieve the effect of CountDownLatch, but the latter cannot be reused after the counter value becomes 0. The former will reset automatically after count == 0, or can manually call reset() method to reset. The former is applicable to the scenarios with unified algorithm but different input parameters. Semaphore adopts the semaphore increment strategy. At the beginning, it does not need to care about the number of synchronization threads. When the acquire method is called, it specifies the number of synchronization threads. It also provides a fair / unfair strategy to obtain semaphores.

Topics: Java IE less