“This is the 30th day of my participation in the First Challenge 2022. For details: First Challenge 2022”

ReentrantReadWriteLock ReentrantReadWriteLock ReentrantReadWriteLock

When the read operation is much higher than the write operation, the read-write lock can be used to enable read-read concurrency and improve performance.

Summary: Improve performance by reducing the granularity of locks. No matter what, even distributed lock, the finer the granularity, the smaller the performance loss caused by it, I don’t know if we can understand it?

This article is based on the form of source code, I hope students can take this as the idea, track the source code step by step debug into, deepen understanding.

First introduction to ReentrantReadWriteLock

Again, take a look at the class diagram:

  • Realize read and write lock interfaceReadWriteLock
  • There are five inner classes, the same as ReentrantLockFairSync,NonfairSyncandSyncAdd two inner classes, both of which implement the Lock interface:
    • WriteLock
    • ReadLock
  • Sync adds two inner classes:
    • HoldCounter: the counter that holds the lock
    • ThreadLocalHoldCounter: Maintains the ThreadLocal of a HoldCounter

Ii. Use cases

A container class that operates on data is usually maintained, and should encapsulate the read and write methods of the data, as follows:

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/ * * *@description: data container class *@author: weirx *@date: 2022/1/13 days *@version: 3.0 * /
public class DataContainer {

    /** * Initializes read and write locks */
    private ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
    private ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
    private ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();

    protected void read(a){
        readLock.lock();
        try {
            System.out.println("Get read lock");
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            readLock.unlock();
            System.out.println("Release read lock"); }}protected void write(a){
        writeLock.lock();
        try {
            System.out.println("Get write lock");
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            writeLock.unlock();
            System.out.println("Release write lock"); }}}Copy the code

Simple test, divided into reading, reading, writing.

  • Read:
    public static void main(String[] args) {
        // Initialize the data container
        DataContainer dataContainer = new DataContainer();

        new Thread(() -> {
            dataContainer.read();
        }, "t1").start();

        new Thread(() -> {
            dataContainer.read();
        }, "t2").start();
    }
Copy the code

As a result, read is not mutually exclusive, and the read lock is acquired and released simultaneously:

Obtain the read lock Obtain the read lock Release the read lock Release the read lockCopy the code
  • Reading and writing:
    public static void main(String[] args) {
        // Initialize the data container
        DataContainer dataContainer = new DataContainer();

        new Thread(() -> {
            dataContainer.read();
        }, "t1").start();

        new Thread(() -> {
            dataContainer.write();
        }, "t2").start();
    }
Copy the code

As a result, read and write are mutually exclusive, and either the read or write methods are executed first, until the read or write lock is released before the next lock is acquired:

Acquire the read lock -- the first execution releases the read lock -- the second execution acquires the write lock -- the third execution releases the write lock -- the fourth executionCopy the code
  • Write about:
    public static void main(String[] args) {
        // Initialize the data container
        DataContainer dataContainer = new DataContainer();

        new Thread(() -> {
            dataContainer.write();
        }, "t1").start();

        new Thread(() -> {
            dataContainer.write();
        }, "t2").start();
    }
Copy the code

As a result, writes are mutually exclusive, and the next write lock cannot be acquired until the first is released:

Obtain write lock Release Write lock Obtain write lock release write lockCopy the code

Note:

  • When a lock is reentered, the write lock is kept waiting after the read lock is obtained
        protected void read(a){
          readLock.lock();
          try {
              System.out.println("Get read lock");
              TimeUnit.SECONDS.sleep(1);
              System.out.println("Get write lock");
              writeLock.lock();
          } catch (InterruptedException e) {
              e.printStackTrace();
          } finally {
              readLock.unlock();
              System.out.println("Release read lock"); }}Copy the code

    Result: No release

    Obtain read lock Obtain write lockCopy the code
  • When a lock is reentered, the write lock is held and the read lock can be acquired again.
     protected void write(a){
          writeLock.lock();
          try {
              System.out.println("Get write lock");
              TimeUnit.SECONDS.sleep(1);
              System.out.println("Get read lock");
              readLock.lock();
          } catch (InterruptedException e) {
              e.printStackTrace();
          } finally {
              writeLock.unlock();
              System.out.println("Release write lock"); }}Copy the code

    Results:

    Obtain write lock Obtain read lock release write lockCopy the code

Third, source code analysis

According to the previous example, from the read lock to release, from the write lock to release, look at the source code.

Note that read-write locks use different bits to distinguish the state of an exclusive lock from that of a shared lock:

       /* * Read and write are divided into two parts, the lower 16 bits are in the exclusive lock state, and the higher 16 bits are in the shared lock state */

        static final int SHARED_SHIFT   = 16;
        static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
        static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
        static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

        /** Returns the number of shares held in count */
        static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
        /** returns the number of mutex holds represented by count */
        static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
Copy the code

3.1 Read Lock Analysis

3.1.1 Obtaining read locks

The lock (); from readLock. Here comes the analysis:

        /** * get read lock. * If the write lock is not held by another thread, the read lock is acquired and returned immediately. * If the write lock is held by another thread, the current thread is disabled for thread scheduling purposes and sleeps until the read lock is acquired */
        public void lock(a) {
            sync.acquireShared(1);
        }
Copy the code

As method of the lock is ReentrantReadWriteLock subclasses ReadLock methods, and acquireShared method is defined in a subclass of AQS Syn, this method attempts to obtain in the form of Shared read locks, failure, into the waiting queue, retry, Until the read lock is acquired.

    public final void acquireShared(int arg) {
        // If held by another thread, go to doAcquireShared of AQS
        if (tryAcquireShared(arg) < 0)
            // Obtain the shared lock. If the lock fails, join the waiting queue
            doAcquireShared(arg);
    }
Copy the code

TryAcquireShared is implemented in ReentrantReadWriteLock.

        protected final int tryAcquireShared(int unused) {
            // Get the current thread
            Thread current = Thread.currentThread();
            // Get the current lock status
            int c = getState();
            // If the exclusive lock count is not equal to 0 and the owner is not the current thread, return -1, in other words, held by another thread
            if(exclusiveCount(c) ! =0&& getExclusiveOwnerThread() ! = current)return -1;
            // Number of shared locks
            int r = sharedCount(c);
            // Return fase to qualify for the read lock
            if(! readerShouldBlock() &&// The number of holdings is smaller than the default value
                r < MAX_COUNT &&
                // CAS sets the lock status
                compareAndSetState(c, c + SHARED_UNIT)) {
                // Hold the shared lock to 0
                if (r == 0) {
                    // The first holder is the current thread
                    firstReader = current;
                    // The total number of holds is 1
                    firstReaderHoldCount = 1;
                } else if (firstReader == current) {
                    // The current thread holds the lock
                    firstReaderHoldCount++;
                } else {
                    // Get the cache count
                    HoldCounter rh = cachedHoldCounter;
                    // If it is null or the id of the holding thread is not the current thread
                    if (rh == null|| rh.tid ! = getThreadId(current))// Assign to the cache
                        cachedHoldCounter = rh = readHolds.get();
                    else if (rh.count == 0)
                        // if rh is not null and is the current thread, set the read lock holder to the value in the cache
                        readHolds.set(rh);
                    // Add + 1
                    rh.count++;
                }
                return 1;
            }
            // The thread that wants to acquire the read lock should block, doing bottom line work, handling failed CAS hits and unprocessed reentrant reads in tryAcquireShared
            return fullTryAcquireShared(current);
        }
Copy the code

We can see from the above source, write lock and read lock is mutually exclusive.

3.1.2 Release of read lock

Get right to the point

    /** * Release lock in shared mode, tryReleaseShared returns true, release */
    public final boolean releaseShared(int arg) {
        / / releases the lock
        if (tryReleaseShared(arg)) {
            // Wake up the next thread in the queue
            doReleaseShared();
            return true;
        }
        return false;
    }
Copy the code

Take a look at the tryReleaseShared implementation of read/write locks:

        protected final boolean tryReleaseShared(int unused) {
            / /... Omit...
            for (;;) {
                int c = getState();
                int nextc = c - SHARED_UNIT;
                if (compareAndSetState(c, nextc))
                    // The count of read locks does not affect other read lock threads, but does affect other write lock threads
                    // The count is 0
                    return nextc == 0; }}Copy the code

If the above method is released successfully, the following AQS inherited method is used:

    private void doReleaseShared(a) {
        // If head.waitStatus == Node.SIGNAL ==> 0 succeeds, the next Node is unpark
        // If head.waitStatus == 0 ==> PROPAGATE
        for (;;) {
            Node h = head;
            if(h ! =null&& h ! = tail) {int ws = h.waitStatus;
              // If another thread is releasing the read lock, change waitStatus to 0 first
              // Prevent the unparksucceeded from being repeated
                if (ws == Node.SIGNAL) {
                    if(! compareAndSetWaitStatus(h, Node.SIGNAL,0))
                        continue; // loop to recheck cases
                    unparkSuccessor(h);
                }
                // If it is already 0, change it to -3
                else if (ws == 0 &&
                        !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue; // loop on failed CAS
            }
            if (h == head) // loop if head changed
                break; }}Copy the code

3.2 Write lock Analysis

3.2.1 acquiring a lock

    public final void acquire(int arg) {
        // Failed to obtain the write lock
        if(! tryAcquire(arg) &&// Associate the current thread to a Node object in exclusive mode
                        // The AQS queue is blockedacquireQueued(addWaiter(Node.EXCLUSIVE), arg) ) { selfInterrupt(); }}Copy the code

Read/write lock lock method: tryAcquire

        protected final boolean tryAcquire(int acquires) {
            Thread current = Thread.currentThread();
            int c = getState();
           // Get the low 16 bits, representing the state count of the write lock
            int w = exclusiveCount(c);
            if(c ! =0) {
                // If the write lock is 0 or the current thread is not equal to the exclusive thread, the acquisition fails
                if (w == 0|| current ! = getExclusiveOwnerThread())return false;
                // Write lock count exceeds the lower 16 bits, an exception is reported
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                // Write the lock to reenter
                setState(c + acquires);
                return true;
            }
            // Write locks should block
            if (writerShouldBlock() ||
                // Failed to change count! compareAndSetState(c, c + acquires))// Failed to obtain the lock
                return false;
            // Set the current thread exclusive lock
            setExclusiveOwnerThread(current);
            return true;
        }
Copy the code

3.2.2 releases the lock

Release:

    public final boolean release(int arg) {
        // Attempted to release the write lock successfully
        if (tryRelease(arg)) {
            Node h = head;
            if(h ! =null&& h.waitStatus ! =0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
Copy the code

TryRelease:

    protected final boolean tryRelease(int releases) {
        if(! isHeldExclusively())throw new IllegalMonitorStateException();
        int nextc = getState() - releases;
        // Because of reentrancy, the write lock count is 0 to be released successfully
        boolean free = exclusiveCount(nextc) == 0;
        if (free) {
            setExclusiveOwnerThread(null);
        }
        setState(nextc);
        return free;
    }
Copy the code

In jdK1.8, StampedLock is also introduced to use StampedLock with stamps to further improve performance. This article is not enough introduction, interested students to collect relevant information to learn.

To learn more about concurrent programming, check out the following column: juejin.cn/column/7050…