Take a look at the read-write lock ReentantReadWriteLock

Keywords: Java less

We saw ReentrantLock earlier. In fact, this lock is only applicable to write more and read less, that is, when multiple threads modify a data, it is suitable to use this lock. However, if multiple threads read a data and use this lock, it will reduce efficiency, because only one thread can read at the same time!

This time, let's look at the ReentantReadWriteLock, which adopts a read-write separation strategy. It is divided into read lock and write lock. Multiple threads can obtain read lock at the same time;

 

1, Simple use of read-write lock

Don't ask me anything. I will use it first. Remember that you implemented a thread safe List with ReentrantLock? We can use the read-write lock to change it a little;

package com.example.demo.study;

import java.util.ArrayList;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class Study0204 {
    // Thread unsafe List
    private ArrayList<String> list = new ArrayList<String>();
    // Read write lock
    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    // Get read lock
    private final Lock readLock = lock.readLock();
    // Get write lock
    private final Lock writeLock = lock.writeLock();

    // Add elements to the collection,Write lock
    public void add(String str) {
        writeLock.lock();
        try {
            list.add(str);
        } finally {
            writeLock.unlock();
        }
    }

    // Delete the elements in the collection,Write lock
    public void remove(String str) {
        writeLock.lock();
        try {
            list.remove(str);
        } finally {
            writeLock.unlock();
        }
    }

    // Gets an element in the collection based on the index,Read lock
    public String get(int index) {
        readLock.lock();
        try {
            return list.get(index);
        } finally {
            readLock.unlock();
        }
    }

}

 

2, Structure of read-write lock

AQS is the core here. You can see that there is still an internal tool class of Sync, and then there are two internal tool classes: read lock ReadLock and write lock WriteLock

 

We can also see that the Sync class inherits AQS, and then there are two classes, NonfairSync and FairSync, to inherit Sync. So far, the structure is the same as ReentrantLock;

 

Let's look at the read Lock and write Lock again. We can see that the Lock interface is implemented, and then all methods in Lock are implemented through the sync object passed in

 

This is the general structure. We can use the following figure to show it. In fact, the most important three classes of ReentrantReadWriteLock are:

One is that the Sync tool class is used to operate AQS blocking queue and state values, and there are fair policies and unfair policies based on Sync implementation;

One is the write Lock, which implements the Lock interface. There is a Sync field inside. In the Lock implementation method, the method of the Sync object is called to implement it

The other is the read Lock. Like the write Lock, it implements the Lock interface. There is a Sync field in it. The implementation methods of Lock are also the method implementation of calling the Sync object

 

II. Analyze Sync

In the last blog, we know that the state in ReentrantLock represents the number of times that a lock can be re entered, and the state is the int type defined in AQS. How do we express the two states in the read-write lock?

Some people will think of some fancy things, let alone useful. Since state is of type int and has 32 bits in total, we can divide it into two parts. The first 16 bits are called high 16 bits, which means the number of times to acquire a read lock, and the last 16 bits are called low 16 bits, which means the number of times to re-enter a write lock. Specifically, we can see the properties of Sync class, mainly involving the base This binary operation, interested can be studied;

abstract static class Sync extends AbstractQueuedSynchronizer {

    //This can be said to be read lock(Shared lock)Number of digits moved
    static final int SHARED_SHIFT   = 16;
    //Read the unit value of lock status. Here is to move 1 signed to the left by 16 bits. 1 is expressed in binary as: 00000000 00000000 00000000 01
    //After moving 16 bits to the left: 00000000 00000001 00000000, that is, the 16th power of 2, that is, 65536
    static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
    //Maximum number of threads reading lock 65535
    static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;

    //Write lock(Exclusive lock)Mask, where the binary representation is 00000000 11111111 11111111
    static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

    //Return the number of read locks, which is state Move the unsigned 16 bits to the right, then the significant number must be the high 16 bits. After turning to decimal system, it is the number of times to acquire the read lock
    static int sharedCount(int c)    { 
        return c >>> SHARED_SHIFT; 
    }

    //Return the number of write locks, here is the state Perform bit by bit and operation with the above write lock mask. The upper 16 bits are set to 0, the 16th significant digit. Turning to decimal is the reentrant number of write locks
    static int exclusiveCount(int c) { 
        return c & EXCLUSIVE_MASK;
    }

    //Omit a lot of code
}

 

Among them, there are several more important attributes in Sync as follows. Don't worry if you don't understand them, just look back when you use them later;

 
 //Record the first thread to acquire the read lock
 private transient Thread firstReader = null;

 //Record the number of reentry times that the first thread obtaining the read lock continues to acquire the read lock
 private transient int firstReaderHoldCount;

 //Record the number of retrievals of the last thread obtaining the read lock, HoldCounter Class is as follows
 private transient HoldCounter cachedHoldCounter;

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

//Record the number of retrievals of the acquired read lock except for the first thread acquiring the read lock, ThreadLocalHoldCounter Class is as follows
private transient ThreadLocalHoldCounter readHolds;

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

 

3, Acquisition and release of write lock

When acquiring a write lock, there is a premise: no other thread holds a write lock or a read lock, so the current thread can acquire a write lock. Otherwise, it will throw the current thread into the blocking queue. Remember, you can't read and write at the same time!

1.lock method:

The write lock is mainly implemented by the internal class WriteLock in ReentantReadWriteLock, which is an exclusive lock. At the same time, only one thread can acquire the lock and re-enter it at any time;

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

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

protected final boolean tryAcquire(int acquires) {
    
    Thread current = Thread.currentThread();
    int c = getState();
    //This method is to realize static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; },Will be state The lower 16 bits of the,
    //That is, the number of reentry times of write lock
    int w = exclusiveCount(c);
    //state Not 0, indicating that the read lock or write lock is occupied
    if (c != 0) {
        //If w==0,and c!=0,Explain c The high 16 of is not 0, that is, a thread has acquired the read lock. At this time, the write lock cannot be acquired. Note that other people cannot write when reading! Return false
        //If w!=0 Indicates that a thread has acquired a write lock,But the thread that occupies the lock is not the current thread. If the thread fails to acquire the write lock, it returns false
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        //This indicates that the write lock can succeed. Then, it is necessary to determine whether the number of reentry times of the write lock is greater than 65535
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        //take state add one-tenth
        setState(c + acquires);
        return true;
    }
    //Come here and explain c==0,That is to say, both the read lock and the write lock are idle. Let's look at the fair strategy and the unfair strategy writerShouldBlock Realization
    if (writerShouldBlock() || !compareAndSetState(c, c + acquires))
        return false;
    setExclusiveOwnerThread(current);
    return true;
}

 

Let's take a look at the if statement at the end. The implementation of writer shouldblock shows that both the read lock and the write lock are idle and can be obtained at any time;

Under the unfair policy, it always returns false, so it will go to compareAndSetState(c, c + acquires), where CAS is used to try to acquire the write lock, and if the acquisition fails, it will return to the sender; if the acquisition succeeds, it will go to setexclusive owner thread (current); the thread setting to occupy the read lock is the current thread;

 

Under the fairness strategy, as I said before, this method is to judge whether there is a precursor node in front of the current thread node. If there is one, it will definitely fail to acquire. To let the precursor node acquire first, it will directly return false in the last if above. If there is no precursor node, it will return true here, and then it will go to the last setexclusiv above Ownerthread (current) sets the write lock occupied by the current thread

 

2.tryLock method

This method is the same as the lock method above. Note that the default mode here and there is unfair mode;

public boolean tryLock( ) {
    return sync.tryWriteLock();
}

final boolean tryWriteLock() {
    Thread current = Thread.currentThread();
    int c = getState();
    if (c != 0) {
        int w = exclusiveCount(c);
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        if (w == MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
    }
    //The default is the unfair mode
    if (!compareAndSetState(c, c + 1))
        return false;
    setExclusiveOwnerThread(current);
    return true;
}

 

 

3.unlock method

public void unlock() {
    sync.release(1);
}
//This is AQS Method in, said tryRelease It's left to the specific subclass to implement, focusing on how to implement it
public final boolean release(int arg) {
if (tryRelease(arg)) {
    Node h = head;
    if (h != null && h.waitStatus != 0)
        unparkSuccessor(h);
    return true;
}
return false;
}

protected final boolean tryRelease(int releases) {
    //isHeldExclusively Method is below because it is called by the current thread release Method, to determine whether the current thread holds the write lock thread, if not, it will be thrown wrong
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    //state Minus one
    int nextc = getState() - releases;
    //Get the lower 16 bits to see if it is equal to 0. If it is equal to 0, it means that there is no thread occupying the write lock at this time, so call setExclusiveOwnerThread(null)
    //Set the thread holding the write lock to null,Last update state All right.
    boolean free = exclusiveCount(nextc) == 0;
    if (free)
        setExclusiveOwnerThread(null);
    setState(nextc);
    return free;
}

protected final boolean isHeldExclusively() {
    return getExclusiveOwnerThread() == Thread.currentThread();
}

 

4, Acquisition and release of read lock

Combined with the previous write lock, let's say:

(1) if no other thread holds a write lock or a read lock, the current thread can acquire the write lock, otherwise it will throw the current thread into the blocking queue; after the current thread obtains the write lock, other threads cannot acquire the write lock and the read lock;

(2) when no other thread acquires the write lock, the current thread can acquire the read lock. Otherwise, it will be thrown into the blocking queue and cannot read and write at the same time. After the current thread acquires the read lock, other threads can only acquire the read lock, not the write lock;

(3) after the current thread obtains the write lock, it can continue to acquire the write lock, which is called reentrant; it can also continue to acquire the read lock, which is called lock degradation;

(4) after the current thread obtains the read lock, it can continue to acquire the read lock;

1.lock method

public void lock() {
    //acquireShared Method in AQS in
    sync.acquireShared(1);
}

public final void acquireShared(int arg) {
    //tryAcquireShared Realization in ReentrantReadWriteLock Medium Sync in
    if (tryAcquireShared(arg) < 0)
        //This method AQS Mainly put the current thread into the blocking queue
        doAcquireShared(arg);
    }
protected final int tryAcquireShared(int unused) {
    
    Thread current = Thread.currentThread();
    int c = getState();
    //Here is the judgment: if other threads acquire the write lock, they will return-1
    //First, judge whether the number of reentrant times of the write lock is not 0, which means that a thread occupies the write lock, and it is not the current thread, then return directly-1
    //Note here: after a thread obtains a write lock, it can acquire a read lock again. When releasing, both of them must be released!!!
    if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
        return -1;
    //This method can only acquire 65535 read locks
    int r = sharedCount(c);
    //readerShouldBlock The method is divided into fair strategy and unfair strategy. The meaning of this method is: the current thread acquisition has acquired the read lock, and the re read lock is blocked, which means that other threads are acquiring the write lock
    //If returns false,It indicates that there is no thread acquiring write lock at this time, and this method can be divided into fair strategy and unfair strategy
    //If the current thread node has a predecessor node, it will return true,No precursor node returned false;
    //If it is unfair, judge whether the node behind the sentinel node in the blocking queue is acquiring the write lock. If it is, return true,If not, return false
    //compareAndSetState(c, c + SHARED_UNIT)Method, SHARED_UNIT 65536, this CAS For the high 16, it means adding 1, for the whole 32-bit, it means adding 2 to the 16th power
    if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) {
        //r==0 Indicates that the read lock is idle, and then records the number of times that the first read lock and the first thread obtaining the read lock can be re entered
        if (r == 0) {
            firstReader = current;
            firstReaderHoldCount = 1;
        //If the current thread is the first thread to acquire the read lock, and then acquire the read lock, then add one more reentry times
        } else if (firstReader == current) {
            firstReaderHoldCount++;
        } else {
            //It means that the read lock has been occupied by other threads. The current thread is the last one to acquire the read lock. Let's update it cacheHoldCounter and readHolds All right.
            //cacheHoldCounter Indicates the number of retrievals of the last thread obtaining the read lock
            //readHolds It records the number of reentrant times that other threads acquire the read lock except the first one
            HoldCounter rh = cachedHoldCounter;
            if (rh == null || rh.tid != getThreadId(current))
                cachedHoldCounter = rh = readHolds.get();
            else if (rh.count == 0)
                readHolds.set(rh);
            rh.count++;
        }
        return 1;
    }
    //Can you come here to explain readerShouldBlock Method returns true,And the current thread has acquired the write lock before, and then acquired the read lock, which is the lock degradation!!!
    return fullTryAcquireShared(current);
}

//Lock degradation operation
final int fullTryAcquireShared(Thread current) {
    HoldCounter rh = null;
    for (;;) {
        int c = getState();
        //If the write lock is occupied by another thread, it will return-1
        if (exclusiveCount(c) != 0) {
            if (getExclusiveOwnerThread() != current)
                return -1;
        //Access to the read lock is blocked. At this time, other threads are accessing the write lock,
        } else if (readerShouldBlock()) {
            if (firstReader == current) {
            } 
            else {
                //There are other threads trying to acquire the write lock. When the current thread finishes acquiring the read lock, update it readHolds All right.
                //Just from readHolds Remove the number of holds of the current thread in, and return-1,End attempt to acquire lock step
                if (rh == null) {
                    rh = cachedHoldCounter;
                    if (rh == null || rh.tid != getThreadId(current)) {
                        rh = readHolds.get();
                        if (rh.count == 0)
                            readHolds.remove();
                    }
                }
                if (rh.count == 0)
                    return -1;
            }
        }
        //When the number of read locks reaches the maximum, it will be thrown wrong
        if (sharedCount(c) == MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        //CAS Update the number of read locks, then update some variables
        if (compareAndSetState(c, c + SHARED_UNIT)) {
            //If the number of read locks is 0, the current thread will be the first thread to acquire the read lock
            if (sharedCount(c) == 0) {
                firstReader = current;
                firstReaderHoldCount = 1;
            //If the current thread has acquired a read lock, add one to the number of retrievals of the first acquired read lock
            } else if (firstReader == current) {
                firstReaderHoldCount++;
            } else {
            //I said that before
                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;
        }
    }
}

 

 

2.tryLock method

public boolean tryLock() {
    return sync.tryReadLock();
}

final boolean tryReadLock() {
    Thread current = Thread.currentThread();
    for (;;) {
        int c = getState();
        //If the current write lock has been occupied, failed to acquire the read lock
        if (exclusiveCount(c) != 0 &&
            getExclusiveOwnerThread() != current)
            return false;
        int r = sharedCount(c);
        //Read lock cannot exceed the maximum number
        if (r == MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        //To update state,Add one higher 16 bits and the update is successful. If the read lock is not occupied by a thread, update the current thread to the first thread to acquire the read lock and update the number of reentry times of the first thread to acquire the read lock
        if (compareAndSetState(c, c + SHARED_UNIT)) {
            if (r == 0) {
                firstReader = current;
                firstReaderHoldCount = 1;
            //The current thread is the first thread to acquire the read lock, and it will increase the reentry times by one
            } else if (firstReader == current) {
                firstReaderHoldCount++;
            } else {
            //This indicates that the current thread has successfully acquired the read lock. Although it is not the first thread to acquire the read lock, update it cachedHoldCounter and readHolds
            //cachedHoldCounter: The number of reentry times that the last thread obtains the read lock
            //readHolds: Remove the first thread, and other threads obtain the reentrant times of read lock
                HoldCounter rh = cachedHoldCounter;
                if (rh == null || rh.tid != getThreadId(current))
                    cachedHoldCounter = rh = readHolds.get();
                else if (rh.count == 0)
                    readHolds.set(rh);
                rh.count++;
            }
            return true;
        }
    }
}

 

 

3.unlock method

public void unlock() {
    sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
    //The implementation is as follows: try to release the read lock, and judge whether there are threads occupying the read lock. If there are no threads occupying the read lock, you will enter the if inside doReleaseShared Method
    if (tryReleaseShared(arg)) {
        //Some threads may be blocked when obtaining the write lock because the current thread read lock is not released
        //The current method is to release one of those threads
        doReleaseShared();
        return true;
    }
    return false;
}
protected final boolean tryReleaseShared(int unused) {
    Thread current = Thread.currentThread();
    //If the current thread is the first one to acquire a read lock
    if (firstReader == current) {
        //If the number of reentrant times for the first thread to acquire the read lock is 1, it will be released. Otherwise, the number of reentrant times will be reduced by one
        if (firstReaderHoldCount == 1)
            firstReader = null;
        else
            firstReaderHoldCount--;
    //The current thread is not the first thread to obtain a read lock cachedHoldCounter and readHolds    
    } else {
        HoldCounter rh = cachedHoldCounter;
        if (rh == null || rh.tid != getThreadId(current))
            rh = readHolds.get();
        int count = rh.count;
        if (count <= 1) {
            readHolds.remove();
            if (count <= 0)
                throw unmatchedUnlockException();
        }
        --rh.count;
    }
    //Here's an infinite loop, get state,Subtract one from the high 16 and use the CAS If the update is successful, judge whether the read lock is occupied
    for (;;) {
        int c = getState();
        int nextc = c - SHARED_UNIT;
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}

 

  

Five. Conclusion

We use the following figure to summarize ReentrantReadWriteLock, which uses state to be 32-bit, high 16 bits to indicate the number of read locks, low 16 bits to indicate the number of re entrances of write locks, and CAS to separate read from write, which is suitable for scenarios with more read and less write;

Posted by young_coder on Thu, 06 Feb 2020 04:15:57 -0800