preface

Compared to Synchronized locks, which require the JVM to implicitly acquire and release locks, Lock locks require display acquisition and release locks, providing more flexibility in acquiring and releasing locks. The basic operation of a Lock Lock is achieved through optimistic locks, but since a Lock Lock can also be suspended when blocked, it is still pessimistic.

In terms of performance, Synchronized Lock has the same performance as Lock due to its advantages of hierarchical Lock in the case of low concurrency and low competition. However, in the case of high load and high concurrency, Synchronized locks are upgraded to heavyweight locks due to fierce competition, and their performance is not as stable as Lock locks. So how does it work?

Lock Implementation principle of the Lock

Lock Java-based Lock is an interface class. Common implementation classes include ReentrantLock, ReentrantReadWriteLock (RRW), and ReentrantLock. They are all dependent on AbstractQueuedSynchronizer (AQS) class.

The AQS class structure contains a linked list-based wait queue (CLH queue) to store all blocked threads, and a state variable in the AQS that represents the locked state for ReentrantLock.

The operation of this queue is realized through CAS operation. We can look at the whole process of obtaining the lock through a picture.

Lock separation optimization Lock synchronization Lock

Although Lock locks are stable, not all scenarios use ReentrantLock exclusive locks for thread synchronization by default.

As we know, for reading and writing the same data, if one thread is reading data while another thread is writing data, the read data and the final data will be inconsistent. If one thread is writing data and another thread is writing data, the data will be inconsistent between the two threads. At this point we can add a mutex to the accessor to ensure that only one thread can read or write at any time.

In most service scenarios, read operations are much larger than write operations. In multithreaded programming, reads do not modify the data of a shared resource, and if multiple threads are merely reading the shared resource, there is no need to lock the resource. Is there any way to optimize the implementation of locks in a scenario where mutex can actually affect the concurrency performance of the business?

1. Read and write lock ReentrantReadWriteLock

For this read-write scenario, Java provides another read-write Lock RRW that implements the Lock interface. We know that ReentrantLock is an exclusive lock that allows only one thread to access it at a time, while RRW allows multiple readers to access it at the same time, but not writer to reader, or writer to writer. Two locks are maintained internally, a ReadLock for read operations and a WriteLock for write operations.

How do read/write locks implement lock separation to ensure atomicity of shared resources?

RRW is also implemented based on AQS, and its custom synchronizer (inheriting AQS) needs to maintain the state of multiple reader threads and one writer thread on the synchronous state, the design of which becomes the key to implementing read and write locks. RRW makes good use of the high and low bits to enable an integer to control two states. Read and write locks split variables into two parts, with high 16 bits indicating read and low 16 bits indicating write.

When a thread attempts to acquire a write lock, it determines whether the synchronization state is 0. If state is 0, no other thread has acquired the lock. If state is not equal to 0, another thread has acquired the lock.

At this point, it is determined whether the lower 16 bits (w) of the synchronization state is 0. If w is 0, it indicates that other threads have acquired the read lock and enter the CLH queue for blocking waiting. If w is not 0, it indicates that another thread has acquired the write lock. At this point, it is necessary to determine whether the current thread has acquired the write lock. If not, it will enter the CLH queue for blocking and waiting. If so, it should determine whether the current thread has acquired the write lock more than the maximum number of times, if so, throw an exception, otherwise update the synchronization status.

When a thread attempts to acquire a read lock, it also determines whether the synchronization state is 0. If state is equal to 0, it indicates that no other thread has acquired the lock temporarily. At this point, it determines whether to block. If so, it enters the CLH queue for blocking waiting. If no blocking is required, the CAS update synchronization status is read.

If state is not 0, the synchronization status is less than 16 bits. If there is a write lock, the lock fails to be obtained and the CLH block queue is entered. Otherwise, it determines whether the current thread should be blocked. If not, the CAS synchronization status is attempted and the synchronization lock is successfully updated to the read state.

Here we go through a square example, to feel the RRW implementation, the code is as follows:

public class TestRTTLock
{
    private double x, y;
    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); / / read lock
    private Lock readLock = lock.readLock(); / / write locks
    private Lock writeLock = lock.writeLock();
    public double read(a)
    {
        // Get read lock
        readLock.lock();
        try
        {
            return Math.sqrt(x * x + y * y);
        }
        finally
        {
            // Release the read lockreadLock.unlock(); }}public void move(double deltaX, double deltaY)
    {
        // Get the write lock
        writeLock.lock();
        try
        {
            x += deltaX;
            y += deltaY;
        }
        finally
        {
            // Release the write lockwriteLock.unlock(); }}}Copy the code

2. Read/write lock re-optimization of StampedLock

RRW is well used in concurrent scenarios where read is greater than write, but there is room for performance improvement with RRW. In the case of many reads and few writes, RRW causes writer threads to experience Starvation, which means that they wait until they are too late to compete for the lock.

In JDK1.8, Java provides the StampedLock class to solve this problem. StampedLock is not implemented based on AQS, but is implemented in the same way as AQS, based on queues and lock states. Unlike RRW, StampedLock control locks have three modes: Write, pessimistic and optimistic. StampedLock will return a ticket stamp when acquiring the lock. The obtained stamp will not only need verification when releasing the lock, but also serve as the second verification after reading the shared resource in optimistic read mode.

Let’s start with an official example to see how StampedLock is used:

public class Point
{
    private double x, y;
    private final StampedLock s1 = new StampedLock();
    void move(double deltaX, double deltaY)
    {
        // Get the write lock
        long stamp = s1.writeLock();
        try
        {
            x += deltaX;
            y += deltaY;
        }
        finally
        {
            // Release the write locks1.unlockWrite(stamp); }}double distanceFormOrigin(a)
    {
        // Optimistic read operation
        long stamp = s1.tryOptimisticRead();
        // Copy variables
        double currentX = x, currentY = y;
        // Check whether there is a write operation during the read
        if(! s1.validate(stamp)) {// Upgrade to pessimistic read
            stamp = s1.readLock();
            try
            {
                currentX = x;
                currentY = y;
            }
            finally{ s1.unlockRead(stamp); }}returnMath.sqrt(currentX * currentX + currentY * currentY); }}Copy the code

We can find: In the process of acquiring a WriteLock, a writer thread first obtains a ticket stamp through WriteLock. WriteLock is an exclusive lock, and only one thread can acquire the lock. When one thread obtains the lock, other threads must wait. A lock can be acquired only if no thread holds a read or write lock. A stamp ticket variable is returned after the lock is successfully requested, which represents the version of the lock. When the lock is released, unlockWrite is required and stamp is passed.

The next step is for a reader thread to acquire the lock. First, the thread will fetch the stamp through the tryOptimisticRead operation. If no thread currently holds a write lock, a non-zero stamp version will be returned. After the thread obtains the stamp, it will copy a shared resource to the method stack. Before that, the specific operations are based on the copy data of the method stack.

If the stamp returned by tryOptimisticRead is currently held by any other thread, validate will return 0 and upgrade to pessimistic lock. Otherwise, the stamp version of the lock can be used to manipulate data.

Compared with RRW, StampedLock only uses and or operations to obtain read locks, and does not involve CAS operations. Even if the optimistic lock fails to obtain for the first time, StampedLock will be upgraded to pessimistic locks immediately. In this way, CPU consumption caused by continuous CAS operations can be avoided. StampedLock is therefore more efficient.

conclusion

Regardless of whether Synchronized or Lock is used, Lock contention will cause thread blocking, resulting in frequent switching between threads, and ultimately increased performance consumption. Therefore, how to reduce lock competition becomes the key to optimize lock.

In Synchronized, we learned that lock contention can be reduced by reducing lock granularity and lock occupancy time. At this point, we know that we can take advantage of the flexibility of the Lock and reduce Lock contention by means of Lock separation.

Lock Lock implements read-write Lock separation to optimize the scenario where read is greater than write. From ordinary RRW implementation to read Lock and write Lock, StampedLock implements optimistic read Lock, pessimistic read Lock and write Lock, all in order to reduce Lock competition and promote the system to achieve the best concurrency performance.