Concurrent programming from scratch - synchronization tool class

Concurrent programming from scratch (ten) - synchronization tool class

6 synchronization tools

6.1 Semaphore

Semaphore, that is, semaphore, provides concurrent access control over the number of resources. Its code is very simple, as shown below:

The function of the parameter method tryAcquire (long timeout, TimeUnit unit) is to try to obtain a license within a specified time. If it cannot be obtained, it returns false.

You can use Semphore to achieve a simple seat Grab:

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Semaphore semaphore = new Semaphore(2);
        for (int i = 0; i < 5; i++) {
            Thread.sleep(500);
            new MySemphore(semaphore).start();
        }

    }
}


public class MySemphore extends Thread{
    private Semaphore semaphore;

    public MySemphore(Semaphore semaphore){
        this.semaphore = semaphore;
    }

    @Override
    public void run() {
        try {
            semaphore.acquire();
            System.out.println(Thread.currentThread().getName()+": working");
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName()+": releasing");
            semaphore.release();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

As shown in the figure below, suppose that there are n threads to obtain 10 resources in Semaphore (n > 10), only 10 of the n threads can obtain them, and other threads will block. No other thread can get the resource until a thread releases the resource.

When the initial number of resources is 1, Semaphore degenerates into an exclusive lock. Because of this, the implementation principle of Semaphone is very similar to that of lock. It is based on AQS and can be divided into fair and unfair. The inheritance system of Semaphore related classes is shown in the following figure:

Since the implementation principles of Semaphore and lock are basically the same. The total number of resources is the initial value of state. In acquire, CAS subtraction is performed on the state variable. After it is reduced to 0, the thread is blocked; Add CAS to the state variable in release.

6.2 CountDownLatch

6.2.1 CountDownLatch usage scenario

Suppose a main thread needs to wait for five Worker threads to finish executing before exiting. You can use CountDownLatch to implement:

Thread:

public class MyThread extends Thread{
    private final CountDownLatch countDownLatch;
    private final Random random = new Random();

    public MyThread(String name ,CountDownLatch countDownLatch){
        super(name);
        this.countDownLatch = countDownLatch;
    }

    @Override
    public void run() {
        try {
            Thread.sleep(random.nextInt(1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " End of operation");
        countDownLatch.countDown();
    }
}

Main class:

public class Main {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(5);
        for (int i = 0; i < 5; i++) {
            new MyThread("THREAD"+i,countDownLatch).start();
        }
        //Current thread waiting
        countDownLatch.await();
        System.out.println("End of program operation");
    }
}

The following figure shows the inheritance hierarchy of related classes of CountDownLatch. The principle of CountDownLatch is similar to that of Semaphore. It is also based on AQS, but there is no distinction between fair and unfair.

6.2.2 await() implementation analysis

await() calls the AQS template method, and CountDownLatch.Sync re implements the tryAccuqireShared method.

From the implementation of the tryacquiresered (...) method, as long as state= 0, the thread calling await() method will be put into the AQS blocking queue and enter the blocking state.

6.2.3 countDown() implementation analysis

Tryrereleaseshared (...) in the AQS template method releaseShared() called by countDown() is implemented by CountDownLatch.Sync. As can be seen from the above code, reduce the value of state through CAS. Only when state=0, tryrereleaseshared (...) will return true, and then execute doReleaseShared(...) to wake up all blocked threads in the queue at one time.

Summary: because it is implemented based on AQS blocking queue, multiple threads can be blocked under the condition of state=0. Reduce the state through countDown(), and wake up all threads at once after it is reduced to 0. As shown in the figure below, assuming that the initial total number is m, N threads await(), and M threads countDown(), N threads are awakened after being reduced to 0.

6.3 CyclicBarrier - circulating barrier

6.3.1 usage scenario of cyclicbarrier

Cyclic Barrier literally means a recyclable Barrier. What it needs to do is to block a group of threads when they reach a Barrier (also known as synchronization point). The Barrier will not open until the last thread reaches the Barrier, and all threads intercepted by the Barrier will continue to run.

CyclicBarrier is easy to use:

//Create CyclicBarrier
        CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
        CyclicBarrier cyclicBarrier = new CyclicBarrier(3, new Runnable() {
            @Override
            public void run() {
                // When all threads are awakened, Runnable is executed.
                System.out.println("do something");
            }
        });
        
//The thread enters the barrier to block
cyclicBarrier.await();

Implementation phase operation:

Main:

public class Main {
    public static void main(String[] args) {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(3, new Runnable() {
            @Override
            public void run() {
                System.out.println("finish");
            }
        });
        for (int i = 0; i < 3; i++) {
            new MyThread(cyclicBarrier).start();
        }
    }
}

MyThread:

public class MyThread extends Thread{
    private final CyclicBarrier cyclicBarrier;
    private final Random random = new Random();

    public MyThread(CyclicBarrier cyclicBarrier){
        this.cyclicBarrier = cyclicBarrier;
    }

    @Override
    public void run() {
        try {
            Thread.sleep(random.nextInt(2000));
            System.out.println(Thread.currentThread().getName()+" A begin");
            cyclicBarrier.await();
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName()+" B begin");
            cyclicBarrier.await();
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName()+" C begin");
        } catch (InterruptedException | BrokenBarrierException e) {
            e.printStackTrace();
        }
    }
}

In the whole process, there are 2 synchronization points. Only after all threads have reached the synchronization point, the last incoming thread will wake up all blocked threads.

6.3.2 implementation principle of cyclicbarrier

CyclicBarrier is implemented based on ReentrantLock+Condition

//This internal class is used to indicate the status of the current loop barrier. When broken is true, it indicates that an exception has occurred to the barrier
    private static class Generation {
        boolean broken = false;
    }
    //Display lock inside CyclicBarrier
    private final ReentrantLock lock = new ReentrantLock();
    //Through the Condition variable obtained from the above explicit lock, the barrier can block and wake up multiple threads, which is fully benefited from this Condition
    private final Condition trip = lock.newCondition();
    //Critical value: when the number of threads blocked by the barrier is equal to parties, that is, count=0, the barrier will wake up all currently blocked threads through trip
    private final int parties;
    //Conditional thread: when the barrier is broken, execute the thread before the barrier wakes up all blocked threads through trip. This thread can act as a main thread, and those blocked threads can act as child threads, that is, it can call the main thread when all child threads reach the barrier
    private final Runnable barrierCommand;
    //The internal class Generation variable represents the state of the current CyclicBarrier
    private Generation generation = new Generation();
    //Counter, which is used to calculate how many threads still have not reached the barrier. The initial value should be equal to the critical value parties
    private int count;

Construction method:

Implementation process of await() method:

    //timed: indicates whether the waiting time is set
    //nanos wait time (nanoseconds)
    private int dowait(boolean timed, long nanos)
        throws InterruptedException, BrokenBarrierException,
               TimeoutException {
        final ReentrantLock lock = this.lock;
        //Use the display lock defined by CyclicBarrier to avoid concurrency problems
        lock.lock();
        try {
            //Status of current circulation barrier
            final Generation g = generation;
            //If true, it indicates that an exception occurred before the barrier, and the exception BrokenBarrierException is thrown
            if (g.broken)
                throw new BrokenBarrierException();
            //Is the current thread interrupted
            if (Thread.interrupted()) {
                breakBarrier();//This method resets the count value count to parties, wakes up all blocked threads and changes the state Generation
                throw new InterruptedException();
            }
            //Barrier counter minus one
            int index = --count;
            //If the index is equal to 0, the number of threads reaching the barrier is equal to the number of parties set at the beginning
            if (index == 0) {  // tripped
                boolean ranAction = false;
                try {
                    final Runnable command = barrierCommand;
                    //If the conditional thread is not empty, the conditional thread is executed
                    if (command != null)
                        command.run();
                    ranAction = true;
                    //Wake up all blocked threads and reset the counter count to generate a new state generation
                    nextGeneration();
                    return 0;
                } finally {
                    if (!ranAction)//If ranAction is true, it means that the above code has not finished successfully, and an exception has occurred to the barrier. Call breakBarrier to reset the counter, and set generation.broken=true to indicate the current state
                        breakBarrier();
                }
            }
 
            // When the counter is zero, the Condition's wake-up method is called, or the broken is true, or the thread is interrupted, or when waiting for timeout, an exception pops up
            for (;;) {
                try {
                    //Block the current thread. If timed is false, the waiting time is not set
                    if (!timed)
                        //The thread is blocked indefinitely, and execution will continue only after the wake-up method is called
                        trip.await();
                    else if (nanos > 0L)
                        //Wait for nanos milliseconds
                        nanos = trip.awaitNanos(nanos);
                } catch (InterruptedException ie) {
                    //If an exception occurs when the await method is called, and the CyclicBarrier has not called the nextGeneration() method to reset the counter and generation
                    if (g == generation && ! g.broken) {
                        breakBarrier();//This method will wake up all blocked threads and reset the counter, and setting generation.broken = true indicates that an exception has occurred in the blocker.
                        throw ie;
                    } else {
                        //Interrupt current thread
                        Thread.currentThread().interrupt();
                    }
                }
                //g.broken is true, indicating that an exception has occurred in the obstacle and an exception is thrown
                if (g.broken)
                    throw new BrokenBarrierException();
                //The wake-up operation with index=0 was successfully completed, so the generation was updated through the nextGeneration() method. Since generation is a shared variable in the thread, the current thread is g= generation
                if (g != generation)
                    return index;
                //If timed is true, it means that the thread blocking time is set, and then the time nanos is less than or equal to 0,
                if (timed && nanos <= 0L) {
                    breakBarrier();//At this time, reset the counter and set generation.broken=true to indicate that the obstacle is abnormal
                    throw new TimeoutException();
                }
            }
        } finally {
            lock.unlock();
        }
    }
 
    //Wake up all threads, reset counter count and regenerate generation
    private void nextGeneration() {
        trip.signalAll();
        count = parties;
        generation = new Generation();
    }
 
    //Set generation.broken=true to indicate the exception of the barrier, reset the counter count, and wake up all blocked threads
    private void breakBarrier() {
        generation.broken = true;
        count = parties;
        trip.signalAll();
    }

There are several explanations for the above method:

  1. CyclicBarrier can be reused. Taking the application scenario as an example, there are 10 threads. These 10 threads wait for each other, wake up together when they arrive, and execute the next logic respectively; Then, the 10 threads continue to wait for each other and wake up together. Each round is called a Generation, which is a synchronization point.

  2. CyclicBarrier will respond to the interrupt. If 10 threads fail to arrive, if any thread receives an interrupt signal, all blocked threads will be awakened, which is the breakBarrier() method above. Then count is reset to the initial value (parties) and restarted.

  3. breakBarrier() will only be executed once by the 10th thread (before waking up the other 9 threads), rather than once each of the 10 threads.

6.4 Exchanger

6.4.1 usage scenario

Exchange is used to exchange data between threads. Its code is very simple. It is an exchange(...) method. Examples are as follows:

public class Main {
    private static final Random random = new Random();
    public static void main(String[] args) {
        // Create an exchange object shared by multiple threads
        // Pass the exchange object to three thread objects. Each thread calls exchange in its own run method and takes its data as a parameter.
        // The return value is the parameter passed in by another thread calling exchange
        Exchanger<String> exchanger = new Exchanger<>();
        new Thread("Thread 1") {
            @Override
            public void run() {
                while (true) {
                    try {
                        // If no other thread calls exchange, the thread blocks until another thread calls exchange.
                        String otherData = exchanger.exchange("Exchange data 1");
                        System.out.println(Thread.currentThread().getName() + "obtain<==" + otherData);
                        Thread.sleep(random.nextInt(2000));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();

        new Thread("Thread 2") {
            @Override
            public void run() {
                while (true) {
                    try {
                        String otherData = exchanger.exchange("Exchange data 2");
                        System.out.println(Thread.currentThread().getName() + "obtain<==" + otherData);
                        Thread.sleep(random.nextInt(2000));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();

        new Thread("Thread 3") {
            @Override
            public void run() {
                while (true) {
                    try {
                        String otherData = exchanger.exchange("Exchange data 3");
                        System.out.println(Thread.currentThread().getName() + "obtain<==" + otherData);
                        Thread.sleep(random.nextInt(2000));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();
    }   
}

In the above example, three threads call exchange(...) concurrently and will interact with data in pairs, such as 1 / 2, 1 / 3 and 2 / 3.

6.4.2 realization principle

Like Lock, the core mechanism of exchange is CAS+park/unpark.

park/unpark recommended reading: https://www.cnblogs.com/set-cookie/p/9582547.html

First, in exchange, there are two internal classes: Participant and Node. The code is as follows:

When each thread calls the exchange(...) method to exchange data, it will first create a Node object.

This Node object is the wrapper of the thread, which contains three important fields: the first is the data that the thread wants to interact with, the second is the data exchanged by the other thread, and the last is the thread itself.

A Node can only support data exchange between two threads. To realize parallel data exchange between multiple threads, multiple nodes are required. Therefore, the Node array is defined in exchange:

private volatile Node[] arena;
6.4.3 exchange (V x) implementation analysis

slotExchange is used for one-to-one data exchange, and arena exchange is used in other cases.

In the above method, if arena is not null, it means that arena mode data exchange is enabled. If arena is not null and the thread is interrupted, an exception is thrown.

If arena is not null, but the return value of arena exchange is null, an exception is thrown. The null value exchanged by the other thread is encapsulated as NULL_ITEM object, not null.

If the return value of slotExchange is null and the thread is interrupted, an exception is thrown.

If the return value of slotExchange is null and the return value of areaExchange is null, an exception is thrown.

Implementation of slotExchange:

Implementation of arena exchange:

6.5 Phaser phase shifter

6.5.1 replace CyclicBarrier and CountDownLatch with Phaser

Starting from JDK7, a synchronization tool class Phaser is added, which is more powerful than CyclicBarrier and CountDownLatch.

CyclicBarrier solves the problem that CountDownLatch cannot be reused, but it still has the following shortcomings:

1) The counter value cannot be dynamically adjusted. If the number of threads is not enough to break the barrier, you can only reset or add more threads, which is obviously unrealistic in practical application

2) Each await consumes only one counter value, which is not flexible enough

Phaser is used to solve these problems. Phaser divides the tasks executed by multiple threads into multiple stages. Each stage can have any participant. Threads can register and participate in a stage at any time.

Instead of CountDownLatch:

public class PhaserInsteadOfCountDownLatch {
    public static void main(String[] args) {
        Phaser phaser = new Phaser(5);
        for (int i = 0; i < 5; i++) {
            new Thread("thread-"+(i+1)){
                private final Random random = new Random();

                @Override
                public void run() {
                    System.out.println(getName()+" start");
                    try {
                        Thread.sleep(random.nextInt(1000));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(getName()+" end");
                    phaser.arrive();
                }
            }.start();
        }
        System.out.println("threads start finish");
        phaser.awaitAdvance(phaser.getPhase());
        System.out.println("threads end finish");
    }
}

Instead of CyclicBarrier:

Main:

public class PhaserInsteadOfCyclicBarrier {
    public static void main(String[] args) {
        Phaser phaser = new Phaser(5);
        for (int i = 0; i < 5; i++) {
            new MyThread(phaser).start();
        }
        phaser.awaitAdvance(0);
    }
}

MyThread:

public class MyThread extends Thread{
    private final Phaser phaser;
    private final Random random = new Random();

    public MyThread(Phaser phaser){
        this.phaser = phaser;
    }

    @Override
    public void run() {
        try {
            System.out.println("start a");
            Thread.sleep(500);
            System.out.println("end a");
            phaser.arriveAndAwaitAdvance();
            System.out.println("start b");
            Thread.sleep(500);
            System.out.println("end b");
            phaser.arriveAndAwaitAdvance();
            System.out.println("start c");
            Thread.sleep(500);
            System.out.println("end c");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

arriveAndAwaitAdance() is the combination of arrive() and awaitAdvance(int), which means "I have reached this synchronization point myself, and I have to wait for everyone to reach this synchronization point, and then move forward together".

6.5.2 new features of phaser
  1. Dynamically adjust the number of threads

    The number of threads to be synchronized by CyclicBarrier is specified in the construction method and cannot be changed later. Phaser can dynamically adjust the number of threads to be synchronized during operation. Phaser provides the following methods to increase and decrease the number of threads to be synchronized.

  2. Hierarchical Phaser

    Multiple phasers can form a tree structure as shown in the following figure, which can be realized by passing in the parent Phaser in the construction method.

    public Phaser(Phaser parent, int parties){
    //....
    }
    

    The tree structure is stored through the parent node:

    private final Phaser parent;
    

It can be found that in the internal structure of Phaser, each Phaser records its own parent node, but does not record its own child node list. Therefore, each Phaser knows who its parent node is, but the parent node does not know how many child nodes it has. The operation on the parent node is realized through the child nodes.

How to use the tree Phaser? Consider the following code to form the tree Phaser in the figure below.

Phaser root = new Phaser(2);
Phaser c1 = new Phaser(root,3);
Phaser c2 = new Phaser(root,2);
Phaser c3 = new Phaser(c1,0);

Originally, root has two participants, and then two sub phasers (c1, c2) are added to it. Each sub Phaser will be counted as one participant, and the participants of root will become 2 + 2 = 4. c1 originally had 3 participants. A child Phaser c3 was added to it, and the number of participants became 3 + 1 = 4. The participant of c3 is initially 0, and can be added later by calling the register() method.

Each node on the tree Phaser can be regarded as an independent Phaser, and its operation mechanism is the same as that of a separate Phaser.

The parent Phaser does not need to be aware of the existence of the child Phaser. When the number of participants registered in the child Phaser is greater than 0, it will register itself with the parent node; When the number of participants registered in the child Phaser equals 0, the parent node will be deregistered automatically. The parent Phaser treats the child Phaser as a normal thread.

6.5.3 state variable parsing

After a general understanding of the usage and new features of phaser, let's carefully analyze its implementation principle. Phaser is not implemented based on AQS, but it has the core features of AQS: state variable, CAS operation and blocking queue. Let's start with the state variable.

private volatile long state;

The 64 bit state variable is divided into four parts:

Phaser provides a series of member methods to get several numbers in the figure above from state. Let's take a look at how the state variable is assigned in the construction method:

Among them, the following has been defined:

private static final int EMPTY = 1;
private static final int PHASE_SHIFT = 32;
private static final int PARTIES_SHIFT = 16;

When parties=0, state is given an EMPTY constant, which is 1;

When parties= 0, shift the phase value left by 32 bits; Move the parties to the left by 16 bits; Then, parties are also used as the lowest 16 bits, and the three values are assigned to state.

6.5.4 Treiber Stack

Based on the above state variable, perform CAS operation, block and wake up accordingly. As shown in the following figure, the mainline on the right will call awaitAdvance() to block; The arrive() on the left will perform CAS decrement operation on the state. When the number of unreachable threads decreases to 0, wake up the blocked main thread on the right.

Here, blocking uses a data structure called Treiber Stack instead of AQS's two-way linked list. Treiber Stack is a lock free stack. It is a one-way linked list. Both out of the stack and in the stack are at the head of the linked list. Therefore, only a head pointer is required instead of a tail pointer. The following implementation:

In order to reduce concurrency conflicts, two linked lists, that is, two Treiber stacks, are defined here. When the phase is an odd number of rounds, the blocking thread is placed in the oddQ; When the phase is an even number of rounds, the blocking thread is placed in evenQ. The code is as follows:

6.5.5 arrive() method analysis

Let's see how the arrive() method operates on the state variable and wakes up the thread.

Where, variables are defined:

private static final int ONE_ARRIVAL = 1;
private static final int ONE_PARTY = 1 << PARTIES_SHIFT;
private static final int ONE_DEREGISTER = ONE_ARRIVAL\ONE_PARTY;
private static final int PARTIES_SHIFT = 16;

Both arrive() and arriveAndDeregister() internally call the doArrive(boolean) method.

The difference is that the former only reduces the "number of threads not reached" by 1; The latter reduces "number of threads not reached" and "number of bus processes in the next round" by 1. Let's take a look at the implementation of the doArrive(boolean) method.

The above methods are described as follows:

  1. Two constants are defined as follows. When deregister=false, only the lowest 16 bits need to be subtracted by 1, adj=ONE_ARRIVAL; When deregister=true, the two 16 bits in the lower 32 bits need to be subtracted by 1, adj=ONE_ARRIVAL|ONE_PARTY.
  2. Reduce the number of unreachable threads by 1. After subtraction, if it has not reached 0, do nothing and return directly. If it reaches 0, two things will be done: first, reset the state, reset the number of unreachable threads in the state to the total number of registered threads, and add 1 to phase; Second, wake up the thread in the queue.

Let's take a look at the wake-up method:

Traverse the whole stack. As long as the phase of the node in the stack is not equal to the phase of the current phase, it indicates that the node is not from the current round, but from the previous round and should be released and awakened.

6.5.6 awaitAdvance() method analysis

The following while loop has four branches:

Initially, node==null, enter the first branch to spin. After the spin times are met, a new QNode node will be created;

Then execute the 3rd and 4th branches to stack and block the node respectively.

The ForkJoinPool.managedBlock(ManagedBlocker blocker) method is called to block the thread corresponding to node. Managerdblock is an interface in ForkJoinPool, which is defined as follows:

QNode implements the interface. The implementation principle is Park (), as shown below. The reason why park()/unpark() is not directly used to realize blocking and wake-up, but the ManagedBlocker layer is encapsulated, mainly for convenience. On the one hand, Park () may be awakened by interrupt, on the other hand, Park () with timeout encapsulates both.

I understand that arrive() and awaitAdvance(), arriveAndAwaitAdvance() are a combined version of both.

Tags: Concurrent Programming JUC

Posted on Thu, 28 Oct 2021 12:53:24 -0400 by jennatar