The Thinking Logic of Computer Programs (81) - Concurrent Synchronization Cooperative Tool

Keywords: Java Programming less github

We are 67 quarter and 68 quarter We have implemented some basic thread collaboration mechanisms, which are implemented by using the basic wait/notify. We mentioned that there are some special synchronization tool classes in Java concurrent packages. In this section, we will discuss them.

The tools we will explore include:

  • Read-write lock ReentrantReadWriteLock
  • Semaphore semaphore
  • Countdown Latch
  • Cyclic Barrier

and 71 quarter Introduced display locks and 72 quarter The presentation conditions are similar. They are also based on AQS. AQS can be seen in Section 71. In some specific synchronous collaboration scenarios, they are more convenient and efficient than using the most basic wait/notify to display locks/conditions. Next, we will discuss their basic concepts, usages, uses and basic principles.

Read-write lock ReentrantReadWriteLock

In the previous section, we introduced two kinds of locks. 66 quarter synchronized is introduced. 71 quarter The display lock ReentrantLock is introduced. For access to the same protected object, whether read or write, they require the same lock. In some scenarios, this is unnecessary. The read operations of multiple threads can be completely parallel. In the scenario of more reads and less writes, parallel reading operations can significantly improve performance.

How can read operations be parallel without affecting consistency? The answer is to use read-write locks. In Java concurrent package, the interface ReadWriteLock represents a read-write lock. The main implementation class is ReentrantReadWriteLock, which can be re-entered.

ReadWriteLock is defined as:

public interface ReadWriteLock {
    Lock readLock();
    Lock writeLock();
}

A ReadWriteLock generates two locks, a read lock and a write lock. Read operation uses read lock, write operation uses write lock.

It should be noted that only "read-read" operations can be parallel, "read-write" and "write-write" can not. Only one thread can write. When acquiring a write lock, only no thread can acquire any lock. When holding a write lock, no other thread can acquire any lock. In the absence of write locks held by other threads, multiple threads can acquire and hold read locks.

ReentrantReadWriteLock is a reentrant read-write lock. It has two constructions, as follows:

public ReentrantLock()
public ReentrantLock(boolean fair)

fire means fair or not. If it doesn't pass, it's false. Explicit Lock Section Similar to the introduction, let's not dwell on it.

Let's take a simple example and implement a caching class MyCache using ReentrantReadWriteLock. The code is as follows:

public class MyCache {
    private Map<String, Object> map = new HashMap<>();
    private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private Lock readLock = readWriteLock.readLock();
    private Lock writeLock = readWriteLock.writeLock();

    public Object get(String key) {
        readLock.lock();
        try {
            return map.get(key);
        } finally {
            readLock.unlock();
        }
    }

    public Object put(String key, Object value) {
        writeLock.lock();
        try {
            return map.put(key, value);
        } finally {
            writeLock.unlock();
        }
    }

    public void clear() {
        writeLock.lock();
        try {
            map.clear();
        } finally {
            writeLock.unlock();
        }
    }
}

The code is relatively simple, let's not go into details.

How does the read-write lock work? Read locks and write locks look like two locks. How do they coordinate? Specific implementation is more complex, we outline its ideas.

Internally, they use the same integer variable to represent the state of the lock, 16 bits for read locks, 16 bits for write locks, and a variable to facilitate CAS operations. In fact, there is only one waiting queue for locks.

The acquisition of write locks is to ensure that no other thread currently holds any locks or waits. When a write lock is released, that is, the first thread in the waiting queue is awakened, either for a read lock or for a write lock.

The acquisition of read locks is different. First, as long as the write locks are not held, the read locks can be acquired. In addition, after acquiring the read locks, it checks the waiting queue and wakes up the first thread waiting for the read locks one by one until the first thread waiting for the write locks. If other threads hold write locks, acquiring read locks will wait. After the read lock is released, check whether the number of read and write locks has changed to 0, and if so, wake up the next thread in the waiting queue.

Semaphore semaphore

The locks described earlier all limit access to one resource at a time by only one thread. In reality, resources are often multiple, but each can only be accessed by one thread at the same time, such as restaurant tables, train bathrooms. Some single resources can be accessed concurrently, but the number of accesses sent may affect performance, so we want to limit the number of threads accessed concurrently. In other cases, related to software authorization and billing, different maximum concurrent access is limited for different levels of accounts.

Semaphore is a semaphore class used to solve this problem. It can limit the number of concurrent access to resources. It has two constructions:

public Semaphore(int permits)
public Semaphore(int permits, boolean fair)

fire means fairness, which is similar to what was introduced before, and permits means the number of licenses.

Semaphore's approach is similar to that of locks. There are two main approaches: obtaining and releasing licenses.

//Blocking access to permission
public void acquire() throws InterruptedException
//Blocking access permission without responding to interruption
public void acquireUninterruptibly()
//Bulk acquisition of multiple licenses
public void acquire(int permits) throws InterruptedException
public void acquireUninterruptibly(int permits)
//Attempt to acquire
public boolean tryAcquire()
//Limited waiting time acquisition
public boolean tryAcquire(int permits, long timeout, TimeUnit unit) throws InterruptedException
//Release permit
public void release()

Let's look at a simple example that limits concurrent access to no more than 100 users. The code is as follows:

public class AccessControlService {
    public static class ConcurrentLimitException extends RuntimeException {
        private static final long serialVersionUID = 1L;
    }

    private static final int MAX_PERMITS = 100;
    private Semaphore permits = new Semaphore(MAX_PERMITS, true);

    public boolean login(String name, String password) {
        if (!permits.tryAcquire()) {
            // The number of simultaneous login users exceeds the limit
            throw new ConcurrentLimitException();
        }
        // ..Other validation
        return true;
    }

    public void logout(String name) {
        permits.release();
    }
}

The code is relatively simple, let's not go into details.

It should be noted that if we set permits to 1, you may think that it becomes a general lock, but it is different from a general lock. In general, locks can only be released by threads holding locks, whereas Semaphore represents only a number of permissions, and any thread can call its release method. The main lock implementation class ReentrantLock is reentrant, while Semaphore is not. Each acqui call consumes a license. For example, look at the following code snippet:

Semaphore permits = new Semaphore(1);
permits.acquire();
permits.acquire();
System.out.println("acquired");

The program will block the second acquire call and will never output "acquired".

The basic principle of semaphore is relatively simple, which is also based on AQS. permits denotes the number of locks shared. The acquire method checks whether the number of locks is greater than 0 or greater, then decreases by one, and achieves success. Otherwise, it will wait. release is to add the number of locks to wake up the first waiting thread.

Countdown Latch

We are 68 quarter A simple bolt called MyLatch is implemented using wait/notify. We mentioned that a similar tool, CountDownLatch, has been provided in Java concurrent packages. Its general meaning is that it is equivalent to a door bolt, which is closed at first. All threads wishing to pass through the door need to wait, and then start countdown. When the countdown becomes zero, the door bolt opens and all threads waiting can pass through. It is one-time and cannot be closed again after opening.

CountDownLatch has a count that is passed through the constructor:

public CountDownLatch(int count)

Multiple threads can collaborate based on this count. Its main methods are:

public void await() throws InterruptedException
public boolean await(long timeout, TimeUnit unit) throws InterruptedException
public void countDown() 

await() checks if the count is zero, and if it is greater than 0, it waits. await() can be interrupted, or the longest waiting time can be set. countDown checks the count, returns directly if it is already zero, otherwise decreases the count, and wakes up all waiting threads if the new count becomes zero.

stay 68 quarter We introduce two application scenarios of door bolt, one is to start at the same time, the other is master-slave cooperation. They all have two types of threads that need to be synchronized with each other. Let's use CountDownLatch to re-demonstrate them.

In the simultaneous start scenario, the operator thread waits for the main referee thread to send out the start instruction signal. Once issued, all the athletes threads start at the same time. The initial count is 1. The athletes thread calls await, and the main thread calls countDown. The example code is as follows:

public class RacerWithCountDownLatch {
    static class Racer extends Thread {
        CountDownLatch latch;

        public Racer(CountDownLatch latch) {
            this.latch = latch;
        }

        @Override
        public void run() {
            try {
                this.latch.await();
                System.out.println(getName()
                        + " start run "+System.currentTimeMillis());
            } catch (InterruptedException e) {
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        int num = 10;
        CountDownLatch latch = new CountDownLatch(1);
        Thread[] racers = new Thread[num];
        for (int i = 0; i < num; i++) {
            racers[i] = new Racer(latch);
            racers[i].start();
        }
        Thread.sleep(1000);
        latch.countDown();
    }
}

The code is relatively simple, let's not go into details. In master-slave collaboration mode, the main thread depends on the result of the worker thread and needs to wait for the end of the worker thread. At this time, the initial value is countdown, countDown is called after the end of the worker thread, await is called by the main thread to wait. The example code is as follows:

public class MasterWorkerDemo {
    static class Worker extends Thread {
        CountDownLatch latch;

        public Worker(CountDownLatch latch) {
            this.latch = latch;
        }

        @Override
        public void run() {
            try {
                // simulate working on task
                Thread.sleep((int) (Math.random() * 1000));

                // simulate exception
                if (Math.random() < 0.02) {
                    throw new RuntimeException("bad luck");
                }
            } catch (InterruptedException e) {
            } finally {
                this.latch.countDown();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        int workerNum = 100;
        CountDownLatch latch = new CountDownLatch(workerNum);
        Worker[] workers = new Worker[workerNum];
        for (int i = 0; i < workerNum; i++) {
            workers[i] = new Worker(latch);
            workers[i].start();
        }
        latch.await();
        System.out.println("collect worker results");
    }
}

It should be emphasized here that countDown calls should be placed in the final statement to ensure that they are also called in case of an exception to the worker thread so that the main thread can return from the await call.

Cyclic Barrier

We are 68 quarter A simple assembly point AssemblePoint is implemented using wait/notify, and we mentioned that a similar tool, Cyclic Barrier, has been provided in Java concurrent packages. Its general meaning is that it is equivalent to a fence, all threads need to wait for other threads after reaching the fence, and then pass together after all threads have arrived. It is circular and can be used as repetitive synchronization.

Cyclic Barrier is especially suitable for parallel iterative computing. Each thread is responsible for part of the computation, and then waits for other threads to complete at the fence. After all threads are in place, data and calculation results are exchanged, and then the next iteration is carried out.

Similar to CountDownLatch, it also has a number, but it represents the number of threads involved, which is passed through the construction method:

public CyclicBarrier(int parties)

It also has a constructor that accepts a Runnable parameter, as follows:

public CyclicBarrier(int parties, Runnable barrierAction)

This parameter represents the fence action. When all threads reach the fence, the action in the parameter is run before all threads perform the next action. This action is performed by the last thread that reaches the fence.

The main method of Cyclic Barrier is await:

public int await() throws InterruptedException, BrokenBarrierException
public int await(long timeout, TimeUnit unit) throws InterruptedException, BrokenBarrierException, TimeoutException

Await waits for other threads to reach the fence and calls await to indicate that it has arrived. If it is the last one to arrive, it executes an optional command, wakes up all waiting threads, and resets the internal synchronization count for recycling.

Await can be interrupted to limit the longest waiting time and throw exceptions after interruption or timeout. The exception Broken Barrier Exception, which means that the fence has been destroyed, what does it mean? In Cyclic Barrier, participating threads interact with each other. As long as one of the threads interrupts or times out when calling await, the fence will be destroyed. In addition, if the fence action throws an exception, the fence will be destroyed. After being destroyed, all threads calling await will exit and throw BrokenBar. RierException.

Let's look at a simple example where multiple visitor threads are synchronized at assembly points A and B respectively:

public class CyclicBarrierDemo {
    static class Tourist extends Thread {
        CyclicBarrier barrier;

        public Tourist(CyclicBarrier barrier) {
            this.barrier = barrier;
        }

        @Override
        public void run() {
            try {
                // The simulation runs independently first
                Thread.sleep((int) (Math.random() * 1000));

                // Marshal Point A
                barrier.await();

                System.out.println(this.getName() + " arrived A "
                        + System.currentTimeMillis());

                // Set-up simulation runs independently
                Thread.sleep((int) (Math.random() * 1000));

                // Marshal Point B
                barrier.await();
                System.out.println(this.getName() + " arrived B "
                        + System.currentTimeMillis());
            } catch (InterruptedException e) {
            } catch (BrokenBarrierException e) {
            }
        }
    }

    public static void main(String[] args) {
        int num = 3;
        Tourist[] threads = new Tourist[num];
        CyclicBarrier barrier = new CyclicBarrier(num, new Runnable() {

            @Override
            public void run() {
                System.out.println("all arrived " + System.currentTimeMillis()
                        + " executed by " + Thread.currentThread().getName());
            }
        });
        for (int i = 0; i < num; i++) {
            threads[i] = new Tourist(barrier);
            threads[i].start();
        }
    }
}

One output on my computer is:

all arrived 1490053578552 executed by Thread-1
Thread-1 arrived A 1490053578555
Thread-2 arrived A 1490053578555
Thread-0 arrived A 1490053578555
all arrived 1490053578889 executed by Thread-0
Thread-0 arrived B 1490053578890
Thread-2 arrived B 1490053578890
Thread-1 arrived B 1490053578890

Multiple threads arrive at A and B at the same time. Cyclic Barrier is used to achieve repetitive synchronization.

Cyclic Barrier and Count Down Latch can be easily confused, and we emphasize the difference between them:

  • CountDownLatch's participating threads have different roles, some are responsible for countdown, some are waiting for the countdown to become zero, and there are many threads responsible for countdown and waiting for countdown. It is used for synchronization between threads with different roles.
  • Cyclic Barrier's role in participating threads is the same, which is used to coordinate threads in the same role.
  • CountDownLatch is one-off, and Cyclic Barrier is reusable.

Summary

This section describes some synchronous collaboration tools in Java concurrent packages:

  • Use ReentrantReadWriteLock instead of ReentrantLock to improve performance in scenarios with more reads and fewer writes
  • Use Semaphore to limit concurrent access to resources
  • Using CountDownLatch to Synchronize Threads with Different Roles
  • Using Cyclic Barrier to achieve consistency between threads in the same role

In practice, these tools should be used preferentially, rather than manually using wait/notify or displaying lock / condition synchronization.

In the next section, let's explore a special concept, ThreadLocal, a thread local variable. What is it?

(As in other chapters, all the code in this section is located at https://github.com/swiftma/program-logic)

----------------

To be continued, check the latest articles, please pay attention to the Wechat public number "Lao Ma Says Programming" (scanning the two-dimensional code below), from the entry to advanced, in-depth shallow, Lao Ma and you explore the essence of Java programming and computer technology. Be original and reserve all copyright.

Posted by shefali on Sun, 07 Jul 2019 15:24:12 -0700