Another article after the holiday: StampedLock (recommended Collection)

Posted by funguse on Wed, 09 Feb 2022 04:34:19 +0100

Hello, I'm glacier~~

I believe most of my friends have a holiday. Binghe once said: vacation is the best time to surpass others, work quietly, and then amaze everyone.

Click the card above to follow me

Some time ago, I wanted to continue[ Proficient in high concurrency series ]Topic, there has been no time, so this matter has been stranded. Now that the manuscript has been written, there will be some time to continue the topic. Before, I put[ Proficient in high concurrency series ]The articles on the topic have been sorted into an e-book - "in depth understanding of high concurrency programming". The contents of the whole book are as follows.

Let's start today, let's continue[ Proficient in high concurrency series ]Special topics. Today, I'd like to introduce a lock with higher performance than read-write lock in a high concurrency environment. Maybe many kids don't know what StampedLock is. At least many kids around me haven't used StampedLock lock lock. Today, let's talk about StampedLock, which is faster than ReadWriteLock in a high concurrency environment.

What is StampedLock?

The ReadWriteLock lock lock allows multiple threads to read shared variables at the same time, but when reading shared variables, other threads are not allowed to write more shared variables. It is more suitable for environments with more reading and less writing. So, in the environment of more reading and less writing, is there a faster lock than ReadWriteLock?

Of course the answer is yes! That's the protagonist we're going to introduce today - jdk1 StampedLock added in 8! Yes, that's it!

Compared with ReadWriteLock, StampedLock also allows a subsequent thread to obtain a write lock and write shared variables during reading. In order to avoid inconsistent data read, when using StampedLock to read shared variables, it is necessary to check whether there are writes to shared variables, and this reading is an optimistic reading.

In short, StampedLock is a kind of lock that allows a subsequent thread to obtain a write lock to write to the shared variable in the process of reading the shared variable, uses optimistic reading to avoid the problem of data inconsistency, and is faster than ReadWriteLock in the high concurrency environment of more reading and less writing.

StampedLock three lock modes

Here, we can make a simple comparison between StampedLock and ReadWriteLock. ReadWriteLock supports two lock modes: one is read lock and the other is write lock. ReadWriteLock allows multiple threads to read shared variables at the same time. When reading, it is not allowed to write. When writing, it is not allowed to read. Reading and writing are mutually exclusive. Therefore, the read lock in ReadWriteLock refers to pessimistic read lock more.

StampedLock supports three lock modes: write lock, read lock (read lock here refers to pessimistic read lock) and optimistic read (many materials and books write optimistic read lock. Here I personally think the more accurate one is optimistic read. Why? Let's continue to read). Among them, the semantics of write lock and read lock are similar to those in ReadWriteLock. Multiple threads are allowed to obtain read lock at the same time, but only one thread is allowed to obtain write lock. Write lock and read lock are also mutually exclusive.

Another difference from ReadWriteLock is that StampedLock will return a variable of Long type after obtaining the read lock or write lock successfully. Then, when releasing the lock, you need to pass in the variable of Long type. For example, the logic shown in the pseudocode below demonstrates how StampedLock acquires and releases locks.

public class StampedLockDemo{
    //Create a StampedLock lock object
    public StampedLock stampedLock = new StampedLock();
    
    //Acquire and release read lock
    public void testGetAndReleaseReadLock(){
        long stamp = stampedLock.readLock();
        try{
            //Execute the business logic after obtaining the read lock
        }finally{
            //Release lock
            stampedLock.unlockRead(stamp);
        }
    }
    
    //Acquire and release write lock
    public void testGetAndReleaseWriteLock(){
        long stamp = stampedLock.writeLock();
        try{
            //Execute the business logic after obtaining the write lock.
        }finally{
            //Release lock
            stampedLock.unlockWrite(stamp);
        }
    }
}

StampedLock supports optimistic reading, which is the key to its better performance than ReadWriteLock. When ReadWriteLock reads a shared variable, all writes to the shared variable will be blocked. The optimistic read provided by StampedLock allows one thread to write to the shared variable when multiple threads read the shared variable.

Let's take another look at the StampedLock example officially given by JDK, as shown below.

class Point {
    private double x, y;
    private final StampedLock sl = new StampedLock();

    void move(double deltaX, double deltaY) { // an exclusively locked method
        long stamp = sl.writeLock();
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            sl.unlockWrite(stamp);
        }
    }

    double distanceFromOrigin() { // A read-only method
        long stamp = sl.tryOptimisticRead();
        double currentX = x, currentY = y;
        if (!sl.validate(stamp)) {
            stamp = sl.readLock();
            try {
                currentX = x;
                currentY = y;
            } finally {
                sl.unlockRead(stamp);
            }
        }
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }

    void moveIfAtOrigin(double newX, double newY) { // upgrade
        // Could instead start with optimistic, not read mode
        long stamp = sl.readLock();
        try {
            while (x == 0.0 && y == 0.0) {
                long ws = sl.tryConvertToWriteLock(stamp);
                if (ws != 0L) {
                    stamp = ws;
                    x = newX;
                    y = newY;
                    break;
                }
                else {
                    sl.unlockRead(stamp);
                    stamp = sl.writeLock();
                }
            }
        } finally {
            sl.unlock(stamp);
        }
    }
}

In the above code, if another thread writes to the shared variable during the optimistic read operation, the optimistic read will be upgraded to a pessimistic read lock, as shown in the following code fragment.

double distanceFromOrigin() { // A read-only method
    //Optimistic reading
    long stamp = sl.tryOptimisticRead();
    double currentX = x, currentY = y;
    //Determine whether a thread has written to the variable
    //If a thread writes to a shared variable
    //Then sl.validate(stamp) will return false
    if (!sl.validate(stamp)) {
        //Upgrade optimistic read lock to pessimistic read lock
        stamp = sl.readLock();
        try {
            currentX = x;
            currentY = y;
        } finally {
            //Release pessimistic lock
            sl.unlockRead(stamp);
        }
    }
    return Math.sqrt(currentX * currentX + currentY * currentY);
}

This method of upgrading optimistic read to pessimistic read lock is more reasonable than the method of always using optimistic read lock. If it is not upgraded to pessimistic read lock, the program will repeatedly execute optimistic read operation in a cycle until no thread performs write operation during optimistic read operation, and continuously executing optimistic read in the cycle will consume a lot of CPU resources, Upgrading to pessimistic read lock is a more reasonable way.

StampedLock implementation idea

StampedLock is internally implemented based on CLH lock. CLH is a spin lock, which can ensure that there is no "hunger phenomenon" and the service order of FIFO (first in first out).

In CLH, locks maintain a queue of waiting threads. All threads that apply for locks but fail to succeed will be stored in this queue. Each node represents a thread and stores a locked flag bit to judge whether the current thread has released the lock. When the locked flag bit is true, it indicates that the lock has been obtained. When the locked flag bit is false, Indicates that the lock was successfully released.

When a thread attempts to obtain a lock, it obtains the tail node of the waiting queue as its preamble node, and uses a code similar to the following to judge whether the preamble node has successfully released the lock:

while (pred.locked) {
    //Omit operation 
}

As long as the preamble node (pred) does not release the lock, it means that the current thread cannot continue to execute, so it will spin and wait; Conversely, if the previous thread has released the lock, the current thread can continue to execute.

This logic is also followed when releasing the lock. The thread will mark the locked position of its own node as false, and the subsequent waiting threads can continue to execute, that is, the lock has been released.

Generally speaking, the implementation idea of StampedLock is relatively simple. I won't talk about it here.

Precautions for StampedLock

In the high concurrency environment with more reads and less writes, the performance of StampedLock is really good, but it can not completely replace ReadWriteLock. When using, we also need to pay special attention to the following aspects.

StampedLock does not support reentry

Yes, StampedLock does not support reentry, that is, when using StampedLock, it cannot be nested, which should be paid special attention to when using.

StampedLock does not support conditional variables

The second thing to note is that StampedLock does not support conditional variables. Whether it is a read lock or a write lock, it does not support conditional variables.

Improper use of StampedLock will cause CPU surge

This is also the most important point. Special attention should be paid when using: if a thread is blocked on the readLock() or writeLock() method of StampedLock, calling the interrupt() method of the blocked thread will interrupt the thread, which will cause the CPU to soar to 100%. For example, the following code shows.

public void testStampedLock() throws Exception{
    final StampedLock lock = new StampedLock();
    Thread thread01 = new Thread(()->{
        // Get write lock
        lock.writeLock();
        // Always block here and do not release the write lock
        LockSupport.park();
    });
    thread01.start();
    // Ensure that thread01 obtains the write lock
    Thread.sleep(100);
    Thread thread02 = new Thread(()->
                           //Blocking in pessimistic read lock
                           lock.readLock()
                          );
    thread02.start();
    // Ensure that T2 is blocked in reading lock
    Thread.sleep(100);
    //Interrupt thread thread02
    //This will cause the CPU of thread thread02 to soar
    thread02.interrupt();
    thread02.join();
}

Running the above program will cause the CPU of thread02 thread to soar to 100%.

Here, many small partners don't quite understand why locksupport park(); Will cause thread01 to block forever. Here, glacier draws a thread life cycle diagram for you, as shown below.

Now you see? In the life cycle of a thread, there are several important states that need to be explained.

  • NEW: in the initial state, the thread is built, but the start() method has not been called.
  • RUNNABLE: RUNNABLE state, which can include running state and ready state.
  • BLOCKED: the BLOCKED state. The thread in this state needs to wait for other threads to release the lock or enter synchronized.
  • WAITING: indicates the WAITING state. The thread in this state needs to wait for other threads to notify or interrupt it, and then enter the next state.
  • TIME_WAITING: timeout waiting state. You can return by yourself at a certain time.
  • TERMINATED: TERMINATED status. The current thread has finished executing.

After reading the life cycle diagram of this thread, you can know why locksupport is called park(); Will it block thread02?

Therefore, when using StampedLock, you must pay attention to avoid the problem of soaring CPU where the thread is located. So how to avoid it?

That is, when using the readLock() method of StampedLock or the read lock and the writeLock() method to obtain the write lock, you must not call the thread interrupt method to interrupt the thread. If it is inevitable to interrupt the thread, Be sure to use the readlockinterruptible() method of StampedLock to obtain an interruptible read lock and the writelockinterruptible() method of StampedLock to obtain an interruptible write lock.

Finally, for the use of StampedLock, the StampedLock example given by the JDK official is a best practice in itself. Young partners can take a look at the StampedLock example given by the JDK official and have a better understanding of the use mode, underlying principles and core ideas of StampedLock.