ReentrantReadWriteLock Read-write lock analysis

Continue to create, accelerate growth! This is the 4th day of my participation in the "Nuggets Daily New Plan · June Update Challenge", click to view the details of the event

Introduction to Read-Write Locks

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 (more reads and less writes). When there is no write operation, there is no problem with multiple threads reading a resource at the same time, so multiple threads should be allowed to read shared resources at the same time (reading and reading can be concurrent); but if a thread wants to write to these shared resources, it should not Other threads are allowed to read and write to the resource (read-write, write-read, write-write mutex). Read-write locks can provide better concurrency and throughput than exclusive locks in situations where there are more reads than writes.

For this scenario, JAVA's concurrent package provides a read-write lock ReentrantReadWriteLock , which internally maintains a pair of related locks, one for read-only operations, called read locks; one for write operations, called write locks , described as follows: Preconditions for a thread to enter a read lock:

  • No write locks for other threads
  • There is no write request or there is a write request, but the calling thread and the thread holding the lock are the same.

The prerequisites for the thread to enter the write lock:

  • No read locks by other threads
  • No write locks for other threads

The read-write lock has the following three important characteristics:

  • Fairness selectivity : Supports both unfair (default) and fair lock acquisition methods, and throughput is still unfair over fairness.
  • Reentrant: Both read locks and write locks support thread reentrancy. Take the read-write thread as an example: after the read thread acquires the read lock, it can acquire the read lock again. After acquiring the write lock, the writer thread can acquire the write lock again, and can also acquire the read lock.
  • Lock downgrade: Following the sequence of acquiring a write lock, then acquiring a read lock, and finally releasing the write lock, a write lock can be downgraded to a read lock.

After reading the above description, you may be a little dizzy. I will give an example of a previous development order to help you understand. Our order has a concept of main order and sub-order: the main order is coded as orderCode, the sub-order is coded as the subOrderCodecorresponding relationship is 1:N. When I am refunding, I need to support the sub-bills, and the main-bills will be refunded. The dimension of the refund of the sub-order is the refund of the subOrderCodemain order, and the dimension is orderCodethat there may be concurrency, we can orderCodeadd a read-write lock to

  • If it is the case of the refund of the main order, whether the refund of the sub-order is mutually exclusive
  • If it is the case of a refund of a sub-order, it can actually be parallelized, but the sub-order is a subOrderCodedimension , and a subOrderCodemutex of a must be added.

Read-write lock usage

How to store read-write locks at the same time can be stored by the value of state. The upper 16 bits represent the read lock, and the lower 16 bits represent the write lock. For example: 0000 0000 0000 0000 (1<<16) 0000 0000 0000 0000 The upper 16 bits are not 0: there is a read lock c >>>16 The lower 16 bits are not 0: there is a write lock 5

ReadWriteLock interface

We can see that ReentranReadWriteLock has two locks, a read lock and a write lock.image.png

Example of use

Cache operation:

public class ReentrantReadWriteLockCacheTest {

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

    // 获取一个key对应的value
    public static final Object get(String key) {
        r.lock();
        try {
            return map.get(key);
        } finally {
            r.unlock();
        }
    }

    // 设置key对应的value,并返回旧的value
    public static final Object put(String key, Object value) {
        w.lock();
        try {
            return map.put(key, value);
        } finally {
            w.unlock();
        }
    }

    // 清空所有的内容
    public static final void clear() {
        w.lock();
        try {
            map.clear();
        } finally {
            w.unlock();
        }
    }

}

复制代码

In the above example, Cache combines a non-thread-safe HashMap as the implementation of the cache, and uses the read lock and write lock of the read-write lock to ensure that the Cache is thread-safe. In the read operation get(String key) method, a read lock needs to be acquired, which prevents concurrent access to the method without blocking. For the write operation put(String key, Object value) method and clear() method, the write lock must be acquired in advance when updating the HashMap. After the write lock is acquired, other threads are blocked for the acquisition of the read lock and the write lock, and only the write lock After the lock is released, other read and write operations can continue. Cache uses read-write locks to improve the concurrency of read operations, ensure the visibility of all read and write operations for each write operation, and simplify programming.

lock downgrade

Lock downgrade refers to the downgrade of a write lock to a read lock. If the current thread holds the write lock, then releases it, and finally acquires the read lock, this segmented process cannot be called lock downgrade. Lock downgrade refers to the process of holding the (currently owned) write lock, acquiring the read lock, and then releasing the (previously owned) write lock. Lock downgrade can help us get the modified result of the current thread without being destroyed by other threads, preventing the loss of updates.

Example use of lock downgrade

Because the data does not change frequently, multiple threads can process data concurrently. When the data changes, if the current thread senses the data change, it prepares the data, and other processing threads are blocked until the current thread completes the data processing. Preparation.

private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private final Lock readLock = rwl.readLock();
private final Lock writeLock = rwl.writeLock();
private volatile boolean update = false;

public void processData() {
    readLock.lock();
    if (!update) {
        // 必须先释放读锁
        readLock.unlock();
        // 锁降级从写锁获取到开始
        writeLock.lock();
        try {
            if (!update) {
                // TODO 准备数据的流程(略)
                update = true;
            }
            readLock.lock();
        } finally {
            writeLock.unlock();
        }
        
        // 锁降级完成,写锁降级为读锁
    }
    try {
        //TODO  使用数据的流程(略)
    } finally {
        readLock.unlock();
    }
}
复制代码

Precautions:

  • Read locks do not support condition variables
  • No upgrade during reentrancy. Not supported: In the case of holding a read lock, acquiring a write lock will result in a permanent wait
  • Support downgrade during reentrancy: if you hold a write lock, you can acquire a read lock

ReentranReadWriteLock structure

method structure design

image.png

Read and write state design

image.png

Source code analysis

write lock

The method tryAcquireis the lock core logic of the write lock

protected final boolean tryAcquire(int acquires) {
    /*
     * Walkthrough:
     * 1. If read count nonzero or write count nonzero
     *    and owner is a different thread, fail.
     * 2. If count would saturate, fail. (This can only
     *    happen if count is already nonzero.)
     * 3. Otherwise, this thread is eligible for lock if
     *    it is either a reentrant acquire or
     *    queue policy allows it. If so, update state
     *    and set owner.
     */
    Thread current = Thread.currentThread();
    int c = getState();
    // 获取写锁状态
    int w = exclusiveCount(c);
    if (c != 0) {
        // (Note: if c != 0 and w == 0 then shared count != 0)
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        // Reentrant acquire
        // 重入
        setState(c + acquires);
        return true;
    }
    // 获取写锁
    if (writerShouldBlock() ||
        !compareAndSetState(c, c + acquires))
        return false;
    // 设置写锁 owner
    setExclusiveOwnerThread(current);
    return true;
}
复制代码

write lock release

protected final boolean tryRelease(int releases) {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    int nextc = getState() - releases;
    boolean free = exclusiveCount(nextc) == 0;
    if (free)
        setExclusiveOwnerThread(null);
    setState(nextc);
    return free;
}
复制代码

Read lock acquisition

protected final int tryAcquireShared(int unused) {
    /*
     * Walkthrough:
     * 1. If write lock held by another thread, fail.
     * 2. Otherwise, this thread is eligible for
     *    lock wrt state, so ask if it should block
     *    because of queue policy. If not, try
     *    to grant by CASing state and updating count.
     *    Note that step does not check for reentrant
     *    acquires, which is postponed to full version
     *    to avoid having to check hold count in
     *    the more typical non-reentrant case.
     * 3. If step 2 fails either because thread
     *    apparently not eligible or CAS fails or count
     *    saturated, chain to version with full retry loop.
     */
    Thread current = Thread.currentThread();
    int c = getState();
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;
    int r = sharedCount(c);
    if (!readerShouldBlock() &&
        r < MAX_COUNT &&
        compareAndSetState(c, c + SHARED_UNIT)) {
        // 首次获取读锁
        if (r == 0) {
            firstReader = current;
            // 第一个线程重入
            firstReaderHoldCount = 1;
        } else if (firstReader == current) {
            // 重入
            firstReaderHoldCount++;
        } else {
            // 后续线程,通过 ThreadLocal 获取重入次数
            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;
    }
    return fullTryAcquireShared(current);
}
复制代码

fullTryAcquireSharedMethods as below:

final int fullTryAcquireShared(Thread current) {
    /*
     * This code is in part redundant with that in
     * tryAcquireShared but is simpler overall by not
     * complicating tryAcquireShared with interactions between
     * retries and lazily reading hold counts.
     */
    HoldCounter rh = null;
    for (;;) {
        int c = getState();
        if (exclusiveCount(c) != 0) {
            if (getExclusiveOwnerThread() != current)
                return -1;
            // else we hold the exclusive lock; blocking here
            // would cause deadlock.
        } else if (readerShouldBlock()) {
            // Make sure we're not acquiring read lock reentrantly
            if (firstReader == current) {
                // assert firstReaderHoldCount > 0;
            } else {
                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;
            }
        }
        if (sharedCount(c) == MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        if (compareAndSetState(c, c + SHARED_UNIT)) {
            if (sharedCount(c) == 0) {
                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;
        }
    }
}
复制代码

release of read lock

protected final boolean tryReleaseShared(int unused) {
    Thread current = Thread.currentThread();
    if (firstReader == current) {
        // assert firstReaderHoldCount > 0;
        if (firstReaderHoldCount == 1)
            firstReader = null;
        else
            firstReaderHoldCount--;
    } 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;
    }
    for (;;) {
        int c = getState();
        int nextc = c - SHARED_UNIT;
        if (compareAndSetState(c, nextc))
            // 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;
    }
}
复制代码

References

Guess you like

Origin juejin.im/post/7102790951042547726