Java concurrent programming --- read-write lock: ReentrantReadWriteLock

Briefly

The reentrant lock ReentrantLock is an exclusive lock. Only one thread can access the exclusive lock at the same time, but in most scenarios, the service is provided most of the time, and the write service occupies less time. However, there is no data race problem in the read service. If one thread prohibits other threads when reading, it will inevitably lead to performance degradation. Therefore, a read-write lock is provided.

Read-write lock maintainers have 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 with the general exclusive lock: at the same time, multiple reader threads can be allowed to access at the same time, but when the writer thread accesses, all the reader threads and writer threads will be blocked. block.

The main features of the read-write lock:

  • Fairness: Supports fairness and injustice.
  • Reentrancy: Reentrancy is supported. Read-write locks support up to 65535 recursive write locks and 65535 recursive read locks.
  • Lock downgrade: Following the order of acquiring write locks, acquiring read locks and releasing write locks, write locks can be downgraded to read locks

The read-write lock ReentrantReadWriteLock implements the interface ReadWriteLock, which maintains a pair of related locks, one for read-only operations and the other for write operations. Read locks can be held concurrently by multiple reader threads as long as there are no writers. Write locks are exclusive.

public interface ReadWriteLock {
    Lock readLock();
    Lock writeLock();
}

ReadWriteLock defines two methods. readLock() returns the lock for read operations and writeLock() returns the lock for write operations. ReentrantReadWriteLock is defined as follows:

/** 内部类  读锁 */
    private final ReentrantReadWriteLock.ReadLock readerLock;
    /** 内部类  写锁 */
    private final ReentrantReadWriteLock.WriteLock writerLock;

    final Sync sync;

    /** 使用默认(非公平)的排序属性创建一个新的 ReentrantReadWriteLock */
    public ReentrantReadWriteLock() {
        this(false);
    }

    /** 使用给定的公平策略创建一个新的 ReentrantReadWriteLock */
    public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }

    /** 返回用于写入操作的锁 */
    public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
    /** 返回用于读取操作的锁 */
    public ReentrantReadWriteLock.ReadLock  readLock()  { return readerLock; }

	abstract static class Sync extends AbstractQueuedSynchronizer {
        /**
         * 省略其余源代码
         */
    }
    public static class WriteLock implements Lock, java.io.Serializable{
        /**
         * 省略其余源代码
         */
    }

    public static class ReadLock implements Lock, java.io.Serializable {
        /**
         * 省略其余源代码
         */
    }

ReentrantReadWriteLock is the same as ReentrantLock, its lock body is still Sync, and its read lock and write lock are realized by Sync. So ReentrantReadWriteLock actually has only one lock, but the way of acquiring read lock and write lock is different. Its read-write lock is actually two classes: ReadLock and writeLock, both of which are lock implementations.

Design of read and write states

The upper 16 bits indicate read, indicating the number of threads holding read locks (sharedCount)

The lower 16 bits represent writes. Number of write lock reentries (exclusiveCount)

& operation: 1 1 is 1, the rest are 0.

A state of type int is used in ReentrantLock to represent the synchronization state, and the value represents the number of times the lock has been repeatedly acquired by a thread. However, the read-write lock ReentrantReadWriteLock maintains two pairs of locks internally, and a variable needs to be used to maintain multiple states. Therefore, the read-write lock adopts the method of "bit-wise cutting and use" to maintain this variable, and divides it into two parts. The high 16 means reading, and the low 16 means writing. After the split, how does the read-write lock quickly determine the status of the read and write locks? pass as operation. If the current synchronization state is S, then the write state is equal to S & 0x0000FFFF (all the upper 16 bits are erased), and the read state is equal to S >>> 16 (unsigned 0 is shifted to the right by 16 bits). code show as below:

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;

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

write lock

A write lock is an exclusive lock that supports reentrancy.

write lock acquisition

The acquisition of the write lock will eventually call tryAcquire(int arg), which is implemented in the inner class Sync:

 protected final boolean tryAcquire(int acquires) {
        Thread current = Thread.currentThread();
        //当前锁个数
        int c = getState();
        //写锁
        int w = exclusiveCount(c);
        if (c != 0) {
            //c != 0 && w == 0 表示存在读锁
            //当前线程不是已经获取写锁的线程
            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;
    }

Write lock acquisition process

  1. The current thread acquires the write lock and increases the state. 2. The current thread acquires the read lock, or another thread acquires the lock and waits.

This method is roughly the same as ReentrantLock's tryAcquire(int arg), and adds a condition when judging reentrancy: whether the read lock exists. Because it is necessary to ensure that the operation of the write lock is visible to the read lock, if the acquisition of the write lock is allowed in the presence of the read lock, other threads that have acquired the read lock may not be aware of the operation of the current writer thread. Therefore, the write lock can be acquired by the current thread only after the read lock is completely released. Once the write lock is acquired, all other read and write threads will be blocked.

write lock release

After the write lock is acquired, it needs to be released. WriteLock provides the unlock() method to release the write lock:

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

    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

The release of the write lock will eventually call the template method release(int arg) method of AQS. This method first calls the tryRelease(int arg) method to try to release the lock. The tryRelease(int arg) method is defined in the internal class Sync of the read-write lock. as follows:

protected final boolean tryRelease(int releases) {
        //释放的线程不为锁的持有者
        if (!isHeldExclusively())
            throw new IllegalMonitorStateException();
        int nextc = getState() - releases;
        //若写锁的新线程数为0,则将锁的持有者设置为null
        boolean free = exclusiveCount(nextc) == 0;
        if (free)
            setExclusiveOwnerThread(null);
        setState(nextc);
        return free;
    }

The whole process of releasing the lock of the write lock is similar to the exclusive lock ReentrantLock. Each release reduces the write state. When the write state is 0, it means that the write lock has been completely released, so that other threads waiting can continue to access the read-write lock and obtain synchronization. state, and the modification of this writing thread is visible to subsequent threads.

read lock

A read lock is a reentrant shared lock that can be held by multiple threads at the same time. The read lock is always or successfully acquired when no other writer threads access it.

Read lock acquisition

The read lock can be obtained through the lock() method of ReadLock:

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

Sync's acquireShared(int arg) is defined in AQS:

 public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }

tryAcquireShared(int arg) tries to get the read synchronization state. This method is mainly used to get the shared synchronization state, and the return result of >= 0 is returned successfully, otherwise the return result of < 0 is returned.

  protected final int tryAcquireShared(int unused) {
        //当前线程
        Thread current = Thread.currentThread();
        int c = getState();
        //exclusiveCount(c)计算写锁
        //如果存在写锁,且锁的持有者不是当前线程,直接返回-1
        //存在锁降级问题,后续阐述
        if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
            return -1;
        //读锁
        int r = sharedCount(c);

        /*
         * readerShouldBlock():读锁是否需要等待(公平锁原则)
         * r < MAX_COUNT:持有线程小于最大数(65535)
         * compareAndSetState(c, c + SHARED_UNIT):设置读取锁状态
         */
        if (!readerShouldBlock() &&
                r < MAX_COUNT &&
                compareAndSetState(c, c + SHARED_UNIT)) {
            /*
             * holdCount部分后面讲解
             */
            if (r == 0) {
                firstReader = current;
                firstReaderHoldCount = 1;
            } else if (firstReader == current) {
                firstReaderHoldCount++;
            } else {
                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);
    }

Description: readHolds saves the reentrancy times of the current thread. firstReader: The first thread that acquires the read lock is stored separately, which improves efficiency and avoids looking for readHolds.

Read lock acquisition process. 1. The current thread holding the write lock can acquire the read lock, and the rest cannot, return -1. 2. Attempt to acquire the read lock, and set the number of times to acquire the read lock.

The process of acquiring a read lock is slightly more complicated than an exclusive lock. The whole process is as follows:

  • Because there is a lock degradation situation, if there is a write lock and the lock holder is not the current thread, it will directly return to failure, otherwise continue

  • According to the principle of fairness, it is judged whether the read lock needs to be blocked, the number of threads holding the read lock is less than the maximum value (65535), and the setting of the lock state is successful, execute the following code (described below for HoldCounter), and return 1. If the condition is not met, execute fullTryAcquireShared().

    fullTryAcquireShared: // Failed to acquire the read lock, put it in the loop and try again.

  final int fullTryAcquireShared(Thread current) {
        HoldCounter rh = null;
        for (;;) {
            int c = getState();
            //锁降级
            if (exclusiveCount(c) != 0) {
                if (getExclusiveOwnerThread() != current)
                    return -1;
            }
            //读锁需要阻塞
            else if (readerShouldBlock()) {
                //列头为当前线程
                if (firstReader == current) {
                }
                //HoldCounter后面讲解
                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");
            //CAS设置读锁成功
            if (compareAndSetState(c, c + SHARED_UNIT)) {
                //如果是第1次获取“读取锁”,则更新firstReader和firstReaderHoldCount
                if (sharedCount(c) == 0) {
                    firstReader = current;
                    firstReaderHoldCount = 1;
                }
                //如果想要获取锁的线程(current)是第1个获取锁(firstReader)的线程,则将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;
            }
        }
    }

The process of retrying in the loop:

    1. Whether there is lock downgrade, otherwise return -1.
    1. The write lock is free, and the fairness policy decides that the thread should block. 1. If the read lock has been acquired, do not process it. 2. If there is no acquisition, it will block.
  • 3. The write lock is free, and the fairness policy determines that the thread can acquire the lock. If the maximum value is not reached, the fetch can succeed. Continue with successful lock processing.

fullTryAcquireShared(Thread current) will be processed according to "whether blocking wait is required", "whether the shared count of read locks exceeds the limit", etc. If no blocking wait is required and the lock's share count does not exceed the limit, try to acquire the lock through CAS and return 1

release of read lock

Like write locks, read locks also provide unlock() to release read locks:

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

The unlcok() method internally uses the releaseShared(int arg) method of Sync, which is defined in AQS:

public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

Call tryReleaseShared(int arg) to try to release the read lock, which is defined in the Sync inner class of the read-write lock:

   protected final boolean tryReleaseShared(int unused) {
        Thread current = Thread.currentThread();
        //如果想要释放锁的线程为第一个获取锁的线程
        if (firstReader == current) {
            //仅获取了一次,则需要将firstReader 设置null,否则 firstReaderHoldCount - 1
            if (firstReaderHoldCount == 1)
                firstReader = null;
            else
                firstReaderHoldCount--;
        }
        //获取rh对象,并更新“当前线程获取锁的信息”
        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;
        }
        //CAS更新同步状态
        for (;;) {
            int c = getState();
            int nextc = c - SHARED_UNIT;
            if (compareAndSetState(c, nextc))
                return nextc == 0;
        }
    }

HoldCounter

In the process of the read lock acquiring and releasing the lock, we can always see a variable rh (HoldCounter), which plays a very important role in the read lock. We understand that the internal mechanism of unlocking is actually a shared lock. In order to better understand HoldCounter, we temporarily think that it is not a probability of a lock, but is equivalent to a counter. A shared lock operation is equivalent to an operation on the counter. Acquire the shared lock, the counter + 1, release the shared lock, the counter - 1. The shared lock can be released and reentrant only after the thread acquires the shared lock. Therefore, the function of HoldCounter is the number of shared locks held by the current thread. This number must be bound to the thread, otherwise an exception will be thrown when operating other thread locks. Let's first look at the definition of HoldCounter:

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

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325026831&siteId=291194637