Java multithreading - ReentrantReadWriteLock

Keywords: less Java Attribute

1. Introduction to read-write lock

There is such a scenario in reality: there are read and write operations on shared resources, and write operations are not as frequent as read operations. When there is no write operation, there is no problem for multiple threads to read a resource at the same time, so multiple threads should be allowed to read shared resources at the same time; however, if a thread wants to write these shared resources, other threads should not be allowed to read and write the resource.

For this scenario, JAVA's concurrent package provides a read-write lock ReentrantReadWriteLock, which represents two locks, one is a read operation related lock, which is called a shared lock, the other is a write related lock, which is called an exclusive lock. The description is as follows:

Precondition for thread to enter read lock:

There is no write lock for other threads,

There is no write request or write request, but the calling thread and the thread holding the lock are the same.

Prerequisites for a thread to enter a write lock:

No read lock for other threads

No write lock for other threads

There are three important characteristics of read-write lock

(1) Fair selectivity: support unfair (default) and fair lock acquisition, throughput is still unfair better than fair.

(2) Reentry: both read and write locks support thread reentry.

(3) Lock degradation: follow the order of acquiring write lock, acquiring read lock and releasing write lock. Write lock can be degraded to read lock.

2. Source code interpretation

Let's first look at the overall structure of the ReentrantReadWriteLock class:

public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {

    /** Read lock */
    private final ReentrantReadWriteLock.ReadLock readerLock;

    /** Write lock */
    private final ReentrantReadWriteLock.WriteLock writerLock;

    final Sync sync;
    
    /** Create a new ReentrantReadWriteLock using the default (non Fair) sort property */
    public ReentrantReadWriteLock() {
        this(false);
    }

    /** Create a new ReentrantReadWriteLock with the given fairness policy */
    public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }

    /** Return lock for write operation */
    public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
    
    /** Return lock for read operation */
    public ReentrantReadWriteLock.ReadLock  readLock()  { return readerLock; }


    abstract static class Sync extends AbstractQueuedSynchronizer {}

    static final class NonfairSync extends Sync {}

    static final class FairSync extends Sync {}

    public static class ReadLock implements Lock, java.io.Serializable {}

    public static class WriteLock implements Lock, java.io.Serializable {}
}

Class inheritance

public class ReentrantReadWriteLock
        implements ReadWriteLock, java.io.Serializable {}

Note: you can see that ReentrantReadWriteLock implements ReadWriteLock interface, ReadWriteLock interface defines the specification for obtaining read lock and write lock, which needs to be implemented by implementation class; meanwhile, it also implements Serializable interface, which means serialization can be performed, and ReentrantReadWriteLock implements its own serialization logic in the source code.

Inner class of class

ReentrantReadWriteLock has five inner classes, which are also related to each other. The relationships of the inner classes are shown in the following figure.

 

Note: as shown in the above figure, Sync inherits from AQS, NonfairSync inherits from Sync class, FairSync inherits from Sync class (the Boolean value passed in through the constructor determines which Sync instance to construct); ReadLock implements Lock interface and WriteLock also implements Lock interface.

Class Sync:

(1) Class inheritance

abstract static class Sync extends AbstractQueuedSynchronizer {}

Note: the Sync abstract class inherits from the AQS abstract class. The Sync class provides support for ReentrantReadWriteLock.

(2) Inner class of class

There are two internal classes in Sync class, HoldCounter and ThreadLocalHoldCounter. HoldCounter is mainly used with read lock. The source code of HoldCounter is as follows.

// Counter
static final class HoldCounter {
    // count
    int count = 0;
    // Use id, not reference, to avoid garbage retention
    // Gets the value of the TID property of the current thread
    final long tid = getThreadId(Thread.currentThread());
}

Note: HoldCounter mainly has two properties, count and tid. Count represents the number of times a read thread has been re entered, and tid represents the value of the TID field of the thread. This field can be used to uniquely identify a thread. The source code of ThreadLocalHoldCounter is as follows

// Local thread counter
static final class ThreadLocalHoldCounter
    extends ThreadLocal<HoldCounter> {
    // Override the initialization method. In the case of no set, the HoldCounter value is obtained
    public HoldCounter initialValue() {
        return new HoldCounter();
    }
}

Description: ThreadLocalHoldCounter overrides the initialValue method of ThreadLocal. The ThreadLocal class can associate threads with objects. In the case of no set, all the get objects are the HolderCounter objects generated in the initialValue method.

(3) Properties of class

abstract static class Sync extends AbstractQueuedSynchronizer {
    // Version serial number
    private static final long serialVersionUID = 6317671515068378041L;        
    // The high 16 bits are read locks and the low 16 bits are write locks
    static final int SHARED_SHIFT   = 16;
    // Read lock unit
    static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
    // Maximum number of read locks
    static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
    // Maximum number of write locks
    static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
    // Local thread counter
    private transient ThreadLocalHoldCounter readHolds;
    // Cached counters
    private transient HoldCounter cachedHoldCounter;
    // First read thread
    private transient Thread firstReader = null;
    // Count of the first read thread
    private transient int firstReaderHoldCount;
}

Note: this attribute includes the maximum number of read lock and write lock threads. Local thread counter, etc.

// Constructor
Sync() {
    // Local thread counter
    readHolds = new ThreadLocalHoldCounter();
    // Set AQS status
    setState(getState()); // ensures visibility of readHolds
}

Note: the local thread counter and AQS state are set in the Sync constructor.

The design of reading and writing state

In the implementation of reentry lock, synchronization state means the number of times repeatedly acquired by the same thread, that is, a shaping variable to maintain, but the previous representation only means whether to lock, without distinguishing between read lock and write lock. The read-write lock needs to maintain the state of multiple read threads and one write thread in the synchronous state (a plastic variable).

The realization of the read-write lock for the synchronous state is to cut the variable into two parts through "bit by bit" on a shaping variable, the high 16 bits represent read and the low 16 bits represent write.

Assuming the current synchronization status value is S, the operations of get and set are as follows:

(1) Get write status:

S & 0x0000ffff: erase all the high 16 bits

(2) Get read status:

S > >

(3) Write status plus 1:

    S+1

(4) Read status plus 1:

S + (1 < < 16) i.e. S + 0x00010000

In the judgment of code layer, if s is not equal to 0, when the write state (S & 0x0000ffff) and the read state (s > > > 16) are greater than 0, it means that the read lock of the read-write lock has been acquired.

Acquisition and release of write lock

public void lock() {
    sync.acquire(1);
}

public void unlock() {
    sync.release(1);
}

You can see that it is the acquisition and release of exclusive synchronization state of the call, so the real implementation is tryAcquire and tryRelease of Sync.

For the acquisition of write lock, see tryAcquire:

protected final boolean tryAcquire(int acquires) {
    //Current thread
    Thread current = Thread.currentThread();
    //Acquisition state
    int c = getState();
    //Number of write threads (that is, the number of reentries to acquire exclusive locks)
    int w = exclusiveCount(c);
    
    //Current synchronization state! = 0, indicating that other threads have acquired read lock or write lock
    if (c != 0) {
        // If the current state is not 0, then: if the write lock state is 0, then the read lock is occupied and returns false;
        // false if the write lock status is not 0 and the write lock is not held by the current thread
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        
        //Determine whether the same thread obtains the write lock more than the maximum number of times (65535). It supports reentry
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        //Update state
        //At this time, the current thread already holds the write lock, and now it is reentry, so you only need to modify the number of locks.
        setState(c + acquires);
        return true;
    }
    
    //This shows that at this time, c=0, neither the read lock nor the write lock has been acquired
    //writerShouldBlock indicates whether it is blocked
    if (writerShouldBlock() ||
        !compareAndSetState(c, c + acquires))
        return false;
    
    //Set lock to be owned by current thread
    setExclusiveOwnerThread(current);
    return true;
}

The exclusiveCount method indicates the number of threads occupying the write lock. The source code is as follows:

static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

Note: directly perform and operation on state and (2 ^ 16 - 1), which is equivalent to 2 ^ 16 on state module. The number of write locks is represented by the lower 16 bits of state.

As can be seen from the source code, the steps to obtain the write lock are as follows:

(1) First, obtain c and w. c indicates the current lock status; w indicates the number of write threads. Then determine whether the synchronization state is 0. If state!=0, other threads have acquired read lock or write lock, execute (2); otherwise, execute (5).

(2) If the lock status is not zero (c! = 0) and the write lock status is 0 (w = 0), it means that the read lock is occupied by other threads at this time, so the current thread cannot acquire the write lock, and it naturally returns false. Or if the lock state is not zero and the write lock state is not zero, but the thread obtaining the write lock is not the current thread, then the current thread cannot acquire the write lock either.

(3) Determine whether the current thread obtains the write lock more than the maximum number of times. If it does, throw an exception. Otherwise, update the synchronization status (at this time, the current thread has acquired the write lock, and the update is thread safe), and return true.

(4) If the state is 0, neither the read lock nor the write lock is acquired. Judge whether blocking is needed (the fair and the unfair methods are different). Under the unfair strategy, it will always not be blocked. Under the fair strategy, it will be judged (judge whether there are threads with longer waiting time in the synchronization queue, if there is one, it needs to be blocked, otherwise, it does not need to be blocked). If not, it needs to be blocked To block, CAS updates the synchronization status. If CAS succeeds, it returns true. If CAS fails, it means the lock has been robbed by another thread, and it returns false. False if blocking is required.

(5) After obtaining the write lock successfully, set the current thread as the thread occupying the write lock, and return true.

The method flow chart is as follows:

Release of write lock, tryRelease method:

protected final boolean tryRelease(int releases) {
    //Throw an exception if the lock holder is not the current thread
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    //Number of new routes of write lock
    int nextc = getState() - releases;
    //If the exclusive mode reentry number is 0, the exclusive mode is released
    boolean free = exclusiveCount(nextc) == 0;
    if (free)
        //If the number of new threads of the write lock is 0, the lock holder is set to null
        setExclusiveOwnerThread(null);
    //Set the number of new routes of write lock
    //Update exclusive reentry number whether exclusive mode is released or not
    setState(nextc);
    return free;
}

The release process of the write lock is relatively simple: first, check whether the current thread is the holder of the write lock, if not throw an exception. Then check whether the number of threads of the write lock is 0 after release. If it is 0, it means that the write lock is idle. Release the lock resource and set the thread holding the lock to null. Otherwise, release is only a re-entry of the lock, and the thread of the write lock cannot be emptied.

Note: this method is used to release write lock resources. First, it will determine whether the thread is exclusive. If it is not exclusive, an exception will be thrown. Otherwise, the number of write locks after releasing resources will be calculated. If it is 0, it means that the resources are released successfully and will not be occupied. Otherwise, the resources will be occupied. The method flow chart is as follows.

Acquisition and release of read lock

Similar to write lock, the actual implementation of lock and unlock of read lock corresponds to tryAcquireShared and tryreleased shared methods of Sync.

To obtain the read lock, see the tryAcquireShared method

protected final int tryAcquireShared(int unused) {
    // Get current thread
    Thread current = Thread.currentThread();
    // Acquisition state
    int c = getState();
    
    //If the number of write lock threads is! = 0, and the exclusive lock is not the current thread, failure will be returned because there is lock degradation
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;
    // Number of read locks
    int r = sharedCount(c);
    /*
     * readerShouldBlock():Whether to wait for reading lock (fair lock principle)
     * r < MAX_COUNT: Hold threads less than maximum (65535)
     * compareAndSetState(c, c + SHARED_UNIT): Set read lock status
     */
     // Whether the read thread should be blocked and less than the maximum value, and the comparison is set successfully
    if (!readerShouldBlock() &&
        r < MAX_COUNT &&
        compareAndSetState(c, c + SHARED_UNIT)) {
        //r == 0, which means the first read lock thread and the first read lock will not be added to readHolds
        if (r == 0) { // Number of read locks is 0
            // Set first read thread
            firstReader = current;
            // The number of resources occupied by the read thread is 1
            firstReaderHoldCount = 1;
        } else if (firstReader == current) { // The current thread is the first read thread, indicating that the first read lock thread is reentered
            // Number of resources used plus 1
            firstReaderHoldCount++;
        } else { // The number of read locks is not 0 and is not the current thread
            // Get counter
            HoldCounter rh = cachedHoldCounter;
            // The counter is empty or the tid of the counter is not the tid of the currently running thread
            if (rh == null || rh.tid != getThreadId(current)) 
                // Get the counter corresponding to the current thread
                cachedHoldCounter = rh = readHolds.get();
            else if (rh.count == 0) // The count is 0.
                //Add to readHolds
                readHolds.set(rh);
            //Count +1
            rh.count++;
        }
        return 1;
    }
    return fullTryAcquireShared(current);
}

The sharedCount method indicates the number of threads occupying the read lock. The source code is as follows:

static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }

Note: directly move the state to the right by 16 bits, you can get the number of threads reading locks, because the high 16 bits of the state represent the number of read locks, and the corresponding 16th bit represents the number of write locks.

The process of acquiring lock by read lock is slightly more complicated than that of write lock. First, judge whether the write lock is 0 and the current thread does not occupy exclusive lock, and return directly. Otherwise, judge whether the read thread needs to be blocked and whether the number of read locks is less than the maximum value and compare the setting status successfully. If there is no read lock currently, set the first read thread firstReader and firstReaderHoldCount. If there is no read lock currently, set the first read thread firstReader and firstReaderHoldCount If the thread thread is the first read thread, increase the firstReaderHoldCount; otherwise, the value of the HoldCounter object corresponding to the current thread will be set. The flow chart is as follows.

Note: after the update is successful, the current thread reentry number (23 lines to 43 lines of code) will be recorded in the first readerholdcount or this thread copy of readHolds(ThreadLocal type). This is to implement the getReadHoldCount() method added in jdk1.6. This method can obtain the current thread reentry number of shared locks (the total reentry number of multiple threads is recorded in the state). Add This method makes the code more complicated, but its principle is very simple: if there is only one thread at present, you don't need to use ThreadLocal to directly store the reentry number in the member variable firstReaderHoldCount. When there is a second thread coming, you need to use the ThreadLocal variable readHolds. Each thread has its own copy to save itself Of.

fullTryAcquireShared method:

final int fullTryAcquireShared(Thread current) {

    HoldCounter rh = null;
    for (;;) { // Infinite cycle
        // Acquisition state
        int c = getState();
        if (exclusiveCount(c) != 0) { // The number of write threads is not 0
            if (getExclusiveOwnerThread() != current) // Not the current thread
                return -1;
        } else if (readerShouldBlock()) { // Number of write threads is 0 and read threads are blocked
            // Make sure we're not acquiring read lock reentrantly
            if (firstReader == current) { // Current thread is the first read thread
                // assert firstReaderHoldCount > 0;
            } else { // Current thread is not the first read thread
                if (rh == null) { // Counter is not empty
                    // 
                    rh = cachedHoldCounter;
                    if (rh == null || rh.tid != getThreadId(current)) { // The counter is empty or the tid of the counter is not the tid of the currently running thread
                        rh = readHolds.get();
                        if (rh.count == 0)
                            readHolds.remove();
                    }
                }
                if (rh.count == 0)
                    return -1;
            }
        }
        if (sharedCount(c) == MAX_COUNT) // Maximum number of read locks, throw exception
            throw new Error("Maximum lock count exceeded");
        if (compareAndSetState(c, c + SHARED_UNIT)) { // Compare and set successfully
            if (sharedCount(c) == 0) { // Number of read threads is 0
                // Set first read thread
                firstReader = current;
                // 
                firstReaderHoldCount = 1;
            } else if (firstReader == current) {
                firstReaderHoldCount++;
            } else {
                if (rh == null)
                    rh = cachedHoldCounter;
                if (rh == null || rh.tid != getThreadId(current))
                    rh = readHolds.get();
                else if (rh.count == 0)
                    readHolds.set(rh);
                rh.count++;
                cachedHoldCounter = rh; // cache for release
            }
            return 1;
        }
    }
}

Note: in the tryAcquireShared function, if the following three conditions are not met (whether the read thread should be blocked, less than the maximum value, and the comparison setting is successful), the fullTryAcquireShared function will be used to ensure the success of related operations. Its logic is similar to that of tryAcquireShared, so it is no longer cumbersome.

Release of read lock, tryreleased method

protected final boolean tryReleaseShared(int unused) {
    // Get current thread
    Thread current = Thread.currentThread();
    if (firstReader == current) { // Current thread is the first read thread
        // assert firstReaderHoldCount > 0;
        if (firstReaderHoldCount == 1) // The number of resources occupied by the read thread is 1
            firstReader = null;
        else // Reduced resources
            firstReaderHoldCount--;
    } else { // Current thread is not the first read thread
        // Get cached counters
        HoldCounter rh = cachedHoldCounter;
        if (rh == null || rh.tid != getThreadId(current)) // The counter is empty or the tid of the counter is not the tid of the currently running thread
            // Get the counter corresponding to the current thread
            rh = readHolds.get();
        // Acquisition count
        int count = rh.count;
        if (count <= 1) { // Count less than or equal to 1
            // remove
            readHolds.remove();
            if (count <= 0) // Count less than or equal to 0, throw an exception
                throw unmatchedUnlockException();
        }
        // Reduce count
        --rh.count;
    }
    for (;;) { // Infinite cycle
        // Acquisition state
        int c = getState();
        // Acquisition state
        int nextc = c - SHARED_UNIT;
        if (compareAndSetState(c, nextc)) // Compare and set
            // Releasing the read lock has no effect on readers,
            // but it may allow waiting writers to proceed if
            // both read and write locks are now free.
            return nextc == 0;
    }
}

Note: this method indicates that the read lock thread releases the lock. First, judge whether the current thread is the first read thread firstReader. If yes, then judge whether the first read thread occupies a resource number of firstReaderHoldCount of 1. If yes, then set the first read thread firstReader to null. Otherwise, subtract 1 from the first read thread's resource number of firstReaderHoldCount. If the current thread is not the first read thread, then obtain the buffer first Store counter (the counter corresponding to the last read lock thread). If the counter is empty or tid is not equal to the tid value of the current thread, the counter of the current thread will be obtained. If the counter count is less than or equal to 1, the counter corresponding to the current thread will be removed. If the counter count of the counter is less than or equal to 0, an exception will be thrown, and then the count will be reduced. In either case, an infinite loop is entered, which ensures that the state state is set successfully. The flow chart is as follows.

In the process of obtaining and releasing the read lock, there is always an object. At the same time, when the acquiring thread obtains the read lock by + 1 and releases the read lock by - 1, the object is HoldCounter.

To understand HoldCounter, you need to understand read lock. As mentioned earlier, the internal implementation mechanism of read lock is shared lock. For shared lock, we can slightly think that it is not a concept of lock, but more like a concept of counter. A shared lock operation is equivalent to a counter operation. Obtain the shared lock counter + 1 and release the shared lock counter-1. Only when the thread obtains the shared lock can it release and re-enter the shared lock. Therefore, the function of HoldCounter is the number of shared locks held by the current thread. This number must be bound with the thread, otherwise an exception will be thrown when operating other thread locks.

First, read the part of lock acquisition:

if (r == 0) {//r == 0, which means the first read lock thread and the first read lock will not be added to readHolds
    firstReader = current;
    firstReaderHoldCount = 1;
} else if (firstReader == current) {//First read lock thread reentry
    firstReaderHoldCount++;    
} else {    //Non firstReader count
    HoldCounter rh = cachedHoldCounter;//readHoldCounter cache
    //rh == null or rh. TID! = current. Getid(), need to get rh
    if (rh == null || rh.tid != current.getId())    
        cachedHoldCounter = rh = readHolds.get();
    else if (rh.count == 0)
        readHolds.set(rh);  //Add to readHolds
    rh.count++; //Count +1
}

Why do we have a firstRead and firstReaderHoldCount here? Instead of using else code directly? This is for the sake of efficiency. The first reader will not be put into the readHolds. If there is only one read lock, it will avoid finding the readHolds. Maybe this code doesn't quite understand HoldCounter. Let's first look at the definitions of firstReader and firstReaderHoldCount:

private transient Thread firstReader = null;
private transient int firstReaderHoldCount;

These two variables are relatively simple. One represents a thread. Of course, this thread is a special thread. The other is the reentry count of firstReader.

Definition of HoldCounter:

static final class HoldCounter {
    int count = 0;
    final long tid = Thread.currentThread().getId();
}

In HoldCounter, there are only two variables: count and tid. Count represents the counter and tid is the id of the thread. But if you want to bind an object to a thread, it's not enough to only record TID, and HoldCounter can't play the role of binding object at all, just record thread TID.

Indeed, in java, we know that if we want to bind a thread and an object together, only ThreadLocal can implement it. So as follows:

static final class ThreadLocalHoldCounter
    extends ThreadLocal<HoldCounter> {
    public HoldCounter initialValue() {
        return new HoldCounter();
    }
}

Therefore, HoldCounter should be a counter on the binding thread, and threadlocalholdcounter is ThreadLocal bound by the thread. From the above we can see that ThreadLocal binds HoldCounter to the current thread, and HoldCounter also holds the thread ID, so that when the lock is released, we can know whether the last cached HoldCounter in ReadWriteLock is the current thread. The advantage of this is that you can reduce the number of ThreadLocal.get(), which is also a time-consuming operation. It should be noted that the reason why HoldCounter binds thread ID instead of thread object is to avoid that HoldCounter and ThreadLocal are bound to each other and it is difficult for GC to release them (although GC can intelligently discover such references and recycle them, but this requires a certain price). In fact, this is just to help GC recycle objects quickly.

3. summary

Through the above source code analysis, we can find a phenomenon:

When a thread holds a read lock, it cannot acquire a write lock (because when acquiring a write lock, if it finds that the current read lock is occupied, it immediately fails to acquire it, regardless of whether the read lock is held by the current thread or not).

When a thread holds a write lock, the thread can continue to acquire the read lock (if it is found that the write lock is occupied when acquiring the read lock, only when the write lock is not occupied by the current thread will the acquisition fail).

If you think about it carefully, this design is reasonable: when a thread acquires a read lock, other threads may hold the read lock at the same time, so you can't "upgrade" the thread acquiring the read lock to a write lock; for the thread acquiring the write lock, it must have exclusive read-write lock, so it can continue to acquire the read lock. When it acquires the write lock and the read lock at the same time, you can also interpret it first The put write lock continues to hold the read lock, so a write lock is "degraded" to read lock.

Sum up:

If a thread wants to hold both the write lock and the read lock, it must first acquire the write lock and then acquire the read lock; the write lock can be "degraded" to read lock; the read lock cannot be "upgraded" to write lock.

70 original articles published, 7 praised, 10000 visitors+
Private letter follow

Posted by underparnv on Sun, 23 Feb 2020 03:04:35 -0800