Part of this article is excerpted from The Art of Concurrent Programming in Java

Reentrant lock

A ReentrantLock, as its name implies, is a ReentrantLock that allows a thread to repeatedly lock a resource. In addition, the lock supports fair and unfair choices when acquiring locks

The so-called unsupported entry can be considered in the following scenarios: If a thread calls the lock() method again after it has called the lock() method, the thread will block by itself because tryAcquire(int acquires) returns false, causing the thread to block

The synchronize keyword implicitly supports re-entry, such as a recursive method with synchronize modification, in which the thread of execution obtains the lock multiple times after the method is executed. ReentrantLock does not support implicit re-entry like the Synchronize keyword, but when the lock() method is called, a thread that has acquired the lock can call the lock() method again without blocking

1. Implement re-entry

The implementation of the reentry feature needs to solve the following two problems:

  • The thread acquires the lock again

    The lock needs to identify whether the thread acquiring the lock is the thread currently occupying the lock, and if so, it is successfully acquired again

  • The final release of the lock

    The thread repeats the lock n times, and after releasing the lock for the NTH time, other threads can acquire the lock. To implement this feature, use counts should be considered

ReentrantLock combines custom synchronizers to obtain and release locks. Take an unfair lock as an example. The code for obtaining the synchronization status is shown as follows, adding the processing logic for obtaining the synchronization status again

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true; }}// Determine whether the current thread is the thread that acquires the lock
    else if (current == getExclusiveOwnerThread()) {
        // Increments the synchronization value and returns true
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}
Copy the code

Considering that the thread that successfully acquired the lock acquires the lock again and only increases the synchronization state value, this requires ReentrantLock to decrease the synchronization state value when releasing the synchronization state. The code for this method is as follows:

protected final boolean tryRelease(int releases) {
    // Reduce the status value
    int c = getState() - releases;
    if(Thread.currentThread() ! = getExclusiveOwnerThread())throw new IllegalMonitorStateException();
    boolean free = false;
    // When the synchronization status is 0, set the owning thread to null and return true, indicating that the release was successful
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}
Copy the code

2. The difference between fair and unfair lock acquisition

If a lock is fair, the order in which the lock is acquired should conform to the absolute chronological order of the request, known as FIFO. Recall from the previous section that an unfair lock is acquired by the current thread as long as the CAS synchronization status is set successfully. A fair lock is different, and the code looks like this:

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        Hasqueuedtoraise () * This method is used to determine whether the current node has a predecessor * If the method returns true, Indicates that a thread has requested the lock before the current thread * and therefore needs to wait for the precursor thread to release the lock before continuing to acquire it */
        if(! hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true; }}else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}
Copy the code

Read-write lock

The locks mentioned above are basically exclusive locks that allow access by only one thread at a time, while read and write locks allow access by multiple threads at a time. However, when the writer thread accesses, all reader threads and other writer threads are blocked. Read-write locks maintain a pair of locks, a read lock and a write lock. By separating the read lock and the write lock, the concurrency is greatly improved compared to the general exclusive lock

1. Interface example

The following uses a cache example to illustrate the use of read/write locks

public class Cache {

    static Map<String, Object> map = new HashMap<>();
    static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    static Lock r = rwl.readLock();
    static Lock w = rwl.writeLock();

    /** * get the value of a key */
    public static Object get(String key) {
        r.lock();
        try {
            return map.get(key);
        } finally{ r.unlock(); }}/** * sets the value of the key and returns the old value */
    public static Object put(String key, Object value) {
        w.lock();
        try {
            return map.put(key, value);
        } finally{ w.unlock(); }}/** * clear all contents */
    public static void clear(a) {
        w.lock();
        try {
            map.clear();
        } finally{ w.unlock(); }}}Copy the code

2. Design the read and write state

Read/write locks also rely on custom synchronizers for functionality, and the read/write state is its synchronizer state. The custom synchronizer for the read-write lock needs to maintain the state of multiple readers and one writer thread on the synchronization state (an integer variable). For this purpose, the read-write lock splits the variable into two parts, with 16 bits higher for read and 16 bits lower for write

The figure above shows that a thread has acquired a write lock, re-entered it twice, and also acquired a read lock twice in a row. The read and write states can be quickly determined by bit operation. Assuming that the current synchronization state value is S, then:

  • Write state equal to S & 0x0000FFFF (erase all high 16 bits)
  • Read state equals S >>> 16 (unsigned 16 bits to the right)
  • When the write state increases by 1, it equals S plus 1
  • When the read state increases by 1, it is equal to S + (1<<6), that is, S + 0x00010000

According to the division of states, a conclusion can be drawn: when S is not equal to 0, when the write state (S & 0x0000FFFF) is equal to 0, then the read state (S >>> 16) is greater than 0, that is, the read lock has been obtained

3. Obtain and release write locks

A write lock is an exclusive lock that supports reentry. Increases the write state if the current thread has already acquired the write lock. If the current thread has already acquired the write lock when it acquires the write lock, or if the thread is not the thread that acquires the write lock, the current thread enters the wait state and obtains the write lock as follows:

protected final boolean tryAcquire(int acquires) {
    Thread current = Thread.currentThread();
    int c = getState();
    // The exclusiveCount method uses c & 0x0000FFFF to get the number of write states
    int w = exclusiveCount(c);
    if(c ! =0) {
        // If c is not equal to 0 and w is equal to 0, then there is a read lock
        // The current thread is not the thread that acquired the write lock
        if (w == 0|| current ! = getExclusiveOwnerThread())return false;
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        setState(c + acquires);
        return true;
    }
    if(writerShouldBlock() || ! compareAndSetState(c, c + acquires))return false;
    setExclusiveOwnerThread(current);
    return true;
}
Copy the code

Each release of the write lock reduces the write status. When the write status is 0, the write lock is released. In this way, the waiting read-write thread can continue to access the lock, and the changes made by the previous write thread are visible to the subsequent read-write thread

4. Obtain and release the read lock

Read lock is a shared lock that supports re-entry. It can be acquired by multiple threads at the same time. When no other writer thread accesses it, the read lock can always be acquired successfully.

protected final int tryAcquireShared(int unused) {

    for(;;) {
        int c = getState();
        int nextc = c + (1<<16);
        if(nextc < c) {
            throw new Error("Maximum lock count exceeded");
        }
        // If another thread has acquired the write lock, the read acquisition fails
        if(exclusiveCount(c) ! =0&& owner ! = Thread.currentThread()) {return -1;
        }
        if(compareAndSetState(c, nextc)) {
            return 1; }}}Copy the code

Each release of the read lock reduces the read state by 1<<16

5. Lock the drop

Lock degradation refers to the degradation of a write lock to a read lock. If the current thread owns the write lock, then releases it, and finally acquires the read lock, this piecewise completion process is not called lock degradation. Lock degradation is the process of holding a write lock, acquiring a read lock, and then releasing the write lock

public void processData(a) {
    readLock.lock();
    if(! update) {// The read lock must be released first
        readLock.unlock();
        // Lock degradation starts from write lock acquisition
        writeLock.lock();
        try {
			if(! update) {// Data preparation process (omitted)
                update = true;
            }
            readLock.lock();
        } finally{ writeLock.unlock(); }}try {
        // The process of using data
    } finally{ readLock.unlock(); }}Copy the code

In the above example, when the data changes, update (volatile) is set to false, and all threads accessing the processData method are aware of the change, but only one thread can acquire the write lock, and the remaining threads are blocked on the lock method that wrote the lock. After the current thread obtains the write lock and completes the data preparation, it acquires the read lock again and releases the write lock to complete the lock degradation