头图

The principle of the read-write lock ReentrantReadWriteLock

introduce

The difference between ReentrantReadWriteLock and ReentrantLock is that ReentrantLock is exclusive lock , and only one thread can acquire the lock at the same time, but in practice it is more about reading more and writing less. Obviously, ReentrantLock cannot satisfy this situation. and ReentrantReadWriteLock use With the read-write separation strategy, multiple threads can be allowed to read at the same time.

类图

As can be seen from this class diagram, a ReadLock and a WriteLock are maintained inside the read-write lock, which rely on Sync implement specific functions, while Sync inherits from AQS, and also provides fair and unfair implementation.

Below we only introduce unfair implementations

We know that a state is maintained in AQS, while the read and write states need to be maintained in ReentrantReadWriteLock, so how does a state identify the two states of writing and reading?

ReentrantReadWriteLock cleverly uses the high 16 bits of state to represent the read state, that is, the number of times the read lock has been acquired; the low 16 bits are used to represent the reentrant number of threads that have acquired the write lock .

static final int SHARED_SHIFT   = 16;
//共享锁(读锁)状态单位值65536
static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
//共享锁线程最大个数65535
static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
//排它锁(写锁)掩码,二进制,15个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; }

In the ReentrantReadWriteLock class diagram, firstReader used to record the first thread of firstReaderHoldCount that acquires the read lock , 061f3b09acc669 is to record the number of first thread that acquired the read lock, and cachedHoldCounter used to record the last thread that acquired the read lock. The number of times the thread can be reentrant.

readHolds is a ThreadLocal variable, which is used to store the reentrant number of read locks acquired by other threads except the first acquiring read thread.

ThreadLocalHoldCounter inherits ThreadLocal, so the initialValue method returns a HoldCounter object.

static final class ThreadLocalHoldCounter
    extends ThreadLocal<HoldCounter> {
    public HoldCounter initialValue() {
        return new HoldCounter();
    }
}
static final class HoldCounter {
    int count = 0;
    // Use id, not reference, to avoid garbage retention
    final long tid = getThreadId(Thread.currentThread());
}

Acquiring and releasing write locks

📢: Write lock in ReentrantReadWriteLock is implemented WriteLock

void lock()

The write lock is exclusive 161f3b09acc758 lock. Only one thread can acquire the lock at a certain time. If no thread currently acquires the read lock or write lock, the current thread can acquire the write lock and return. If a thread has already acquired a read lock and a write lock, the thread currently requesting the write lock will be blocked and suspended. The write lock is a reentrant lock . If the current thread has acquired the lock, it will only be acquired when it is acquired again. Just add 1 to the number of reentrants.

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

You can see that the acquire method of AQS is called inside the lock, and tryAcquire is overridden by the Sync class inside ReentrantReadWriteLock. as follows:

protected final boolean tryAcquire(int acquires) {
            Thread current = Thread.currentThread();
            int c = getState();
            int w = exclusiveCount(c);
                      //(1) c != 0说明读锁或写锁已经被某线程获取
            if (c != 0) {
                //(2) w == 0 说明已经有线程获取到该锁,w != 0并且当前线程并不是拥有者则返回false
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                //(3) 说明当前线程获取到了写锁,判断可重入次数
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                //(4)可重入次数加一
                setState(c + acquires);
                return true;
            }
            //(5)第一个写线程获取写锁
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
            setExclusiveOwnerThread(current);
            return true;
}

In code (1), if the current AQS status value is not 0, it means that a thread has acquired a read lock or a write lock.

In code (2), if w==0, the lower 16 bits of the status value are 0, and the AQS status value is not 0, implying that a thread has acquired the read lock, so it returns false. And if w!=0, it means that a thread has already acquired the write lock, then see if it is acquired by itself, instead of returning false.

In code (3), it indicates that the current thread has acquired the lock, and it is judged whether the number of reentrants of the thread is greater than the maximum value. If it is, an exception will be thrown, otherwise, execute code (4) to increase the number of reentrants, and then return true .

If the status value of AQS is equal to 0, it means that no thread has acquired the read lock and write lock at present, so code (5) is executed. Among them, for the writerShouldBlock method, the implementation of the unfair lock is as follows:

final boolean writerShouldBlock() {
      return false; // writers can always barge
}

It can be seen that the method returns false, indicating that CAS needs to try to acquire the write lock. If the acquisition is successful, set the current lock holder as the current thread, and then return true, otherwise return false.

Fair locks are implemented as follows:

final boolean writerShouldBlock() {
      return hasQueuedPredecessors();
}
public final boolean hasQueuedPredecessors() {
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
}

Here we still use hasQueuedPredecessors to determine whether the current node has a predecessor node, etc., which has been discussed in the article "ReentrantLock Analysis", so I won't talk about it here.

void lockInterruptibly()

Similar to the lock method, the difference is that it responds to interruptions, that is, when other threads call the thread's interrupt method to interrupt the current thread, the current thread will throw an InterruptedException exception.

public void lockInterruptibly() throws InterruptedException {
    sync.acquireInterruptibly(1);
}

boolean tryLock()

Try to acquire the write lock. If no other thread currently holds the write lock or read lock, the current thread acquires the write lock and returns true; if a thread already acquires the write lock or read lock, it returns false, and the current thread will not block; if The current thread already holds the write lock, then the state value is incremented by 1 and returns true. The implementation is similar to the tryAcquire method, so I won't talk much about it.

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");
    }
    if (!compareAndSetState(c, c + 1))
        return false;
    setExclusiveOwnerThread(current);
    return true;
}

boolean tryLock(long timeout,TimeUnit unit)

The difference from the tryAcquire method is that there is an additional timeout parameter. If the attempt to acquire the write lock fails, the current thread will be suspended for a specified time. After the waiting time expires, the thread will be activated to retry the acquisition, and if the write lock is still not acquired, it will return false. In addition, this method will respond to the interruption, that is, when other threads call the interrupt method of the thread to interrupt the current thread, the current thread will throw an InterruptedException exception.

public boolean tryLock(long timeout, TimeUnit unit)
        throws InterruptedException {
    return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}

void unlock()

Try to release the lock. If the current thread holds the lock, the AQS state value will be decremented by 1 after the call. If the lock is 0 after decrementing 1, the lock will be released, otherwise it will only be decremented by 1. Throws an exception if the current thread does not hold the lock and this method is called.

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;
}
protected final boolean tryRelease(int releases) {
    //判断调用线程是否是写锁拥有者
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
   //获取可重入值,这里没有考虑高16位,因为获取写锁时状态值肯定为0
    int nextc = getState() - releases;
    boolean free = exclusiveCount(nextc) == 0;
    //如果为0则释放锁,否则只是更新状态值
    if (free)
        setExclusiveOwnerThread(null);
    setState(nextc);
    return free;
}

Read lock acquisition and release

📢: The read lock in ReentrantReadWriteLock is implemented ReadLock

void lock()

Acquire the read lock. If the current thread does not hold the lock, the current thread acquires the lock, and the upper 16 bits of the AQS state value state are incremented by 1, and then returns. Otherwise if another thread holds the write lock, the current thread is blocked.

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

In this code, the lock method of the read lock calls the acquireShared method of AQS, and then internally calls the tryAcquireShared method overridden by the Sync class in ReentrantReadWriteLock.

protected final int tryAcquireShared(int unused) {
    Thread current = Thread.currentThread();
    //(1)获取当前状态值
    int c = getState();
    //(2)判断是否被写锁占用
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;
    //(3)获取读锁计数
    int r = sharedCount(c);
    //(4)尝试获取锁,多个读线程同时只有一个会成功,不成功的进入fullTryAcquireShared方法重试
    if (!readerShouldBlock() &&
        r < MAX_COUNT &&
        compareAndSetState(c, c + SHARED_UNIT)) {
        //(5)第一个线程获取锁,设置 firstReader 等于当前线程
        if (r == 0) {
            firstReader = current;
            firstReaderHoldCount = 1;
        //(6)如果当前线程是第一个获取锁的线程则进行第一个线程可重入次数加1
        } else if (firstReader == current) {
            firstReaderHoldCount++;
        } else {
            //(7)记录最后一个获取读锁的线程或其他线程读锁的可重入数
            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;
    }
      //(8)类似tryAcquireShared,但是是自旋获取
    return fullTryAcquireShared(current);
}

The method first obtains the current AQS status value, and then code (2) checks whether other threads have acquired the write lock, and returns -1 if so. Then put into the AQS blocking queue.

If the thread that currently wants to acquire the read lock already holds the write lock, it can also acquire the read lock. However, it should be noted that when a thread first acquires a write lock, and then acquires a read lock, after processing things, it must remember to release both the read lock and the write lock, not just the write lock.

If the code (3) is executed first to obtain the number of read locks, it means that no thread has obtained the write lock at present, but some threads may have obtained the read lock, and then execute the code (4), in which the readerShouldBlock of the unfair lock is implemented. as follows:

final boolean readerShouldBlock() {
      return apparentlyFirstQueuedIsExclusive();
}
final boolean apparentlyFirstQueuedIsExclusive() {
    Node h, s;
    return //头节点不允许为空,也就是阻塞队列存在
            (h = head) != null &&
            //头节点下一个节点必须存在
            (s = h.next)  != null &&
            //下一个节点不能共享,也就是写锁
            !s.isShared()         &&
            //下一个节点的线程对象不允许为空
        s.thread != null;
}

The function of this method is that can be obtained if the blocking queue is empty; if the blocking queue is not empty, there are two cases. One: If the first node is a write node, then you cannot acquire a read lock, blocking the queuing. Second, if the first node is a read node, it can be obtained.

Then, in code (4), it is judged whether the thread currently acquiring the read lock by the current thread has reached the maximum value. A final CAS operation increments the upper 16 bits of the AQS status value by one.

Code (5) (6) records the first thread that acquires the read lock and counts the number of reentrants that the thread acquires the read lock.

Code (7) uses cachedHoldCounter to record the last thread that acquired the read lock and the reentrant number of the read lock acquired by the thread, and readHolds records the reentrant number of the read lock acquired by the current thread.

If the readerShouldBlock method returns true, it means that a thread is acquiring the write lock, so code (8) is executed. The code of fullTryAcquireShared is similar to tryAcquireShared, but the difference is that the former is acquired by cyclic spin.

final int fullTryAcquireShared(Thread current) {
            HoldCounter rh = null;
            for (;;) {
                //获取状态值
                int c = getState();
                //如果存在写锁
                if (exclusiveCount(c) != 0) {
                    //持有者不是自己 返回 -1
                    if (getExclusiveOwnerThread() != current)
                        return -1;
                //如果写锁空闲,且可以获取读锁
                } else if (readerShouldBlock()) {
                    // 第一个读线程是当前线程
                    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");
                // CAS 设置读锁,高位加1
                if (compareAndSetState(c, c + SHARED_UNIT)) {
                    // sharedCount(c) == 0 说明读锁空闲 进行设置
                    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))
                            //创建一个cachedHoldCounter
                            rh = readHolds.get();
                        else if (rh.count == 0)
                            readHolds.set(rh);
                        //计数器自增
                        rh.count++;
                        // 更新缓存计数器
                        cachedHoldCounter = rh; // cache for release
                    }
                    return 1;
                }
            }
}

void lockInterruptibly()

Similar to the lock method, the difference is that this method will respond to interruptions, that is, when other threads call the thread's interrupt method to interrupt the current thread, the current thread will throw an InterruptedException exception.

boolean tryLock()

Attempt to acquire the read lock, if no other thread currently holds the write lock, the current thread will succeed in acquiring the read lock, and then returns true. If another thread currently holds the write lock, the method returns false directly, but the current thread will not be blocked. If the current thread already holds the read lock, simply increase the high 16 bits of the AQS status value and return true directly. Its code is similar to the code of tryLock and will not be described here.

boolean tryLock(long timeout,TimeUnit unit)

The difference from tryLock is that there is an additional timeout parameter. If the attempt to acquire the read lock fails, the current thread will be suspended for a specified time. After the timeout period expires, the current thread will be activated. If the read lock has not been acquired at this time, then Return false. In addition, this method responds to the interrupt, that is, when other threads call the interrupt method of the thread to interrupt the current thread, the current thread will throw an InterruptedException exception.

void unlock()

The release of the read lock is delegated to Sync. The releaseShared method is as follows.

public void unlock() {
    sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}
protected final boolean tryReleaseShared(int unused) {
    Thread current = Thread.currentThread();
    //如果当前线程是第一个线程
    if (firstReader == current) {
        // 如果等于1 说明没有重复获取,则设置为null
        if (firstReaderHoldCount == 1)
            firstReader = null;
        else
          // 否则减1
            firstReaderHoldCount--;
    } else {//如果不是当前线程
        HoldCounter rh = cachedHoldCounter;
        // 如果缓存是 null 或者缓存所属线程不是当前线程,则当前线程不是最后一个读锁
        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();
        // 减去一个读锁。对高16位减1
        int nextc = c - SHARED_UNIT;
        // 修改成功,如果是 0,表示读锁和写锁都空闲,则可以唤醒后面的等待线程
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}

As shown in the above code, in the infinite loop, first obtain the current AQS state value and save it to the variable c, then subtract a read count unit from c and use the CAS operation to update the AQS state value, if the update result is to check whether the current AQS state value is 0, if it is 0, it means that there is no read thread occupying the read lock, then return true.

Then call the doReleaseShared method to release a thread blocked due to acquiring the write lock. If the current AQS status value is not 0, it means that there are other reading threads, so return false.

Upgrading and downgrading of locks

Upgrading and downgrading means that a read lock is upgraded to a write lock, and a write lock is downgraded to a read lock. In the ReentrantReadWriteLock read-write lock, only write locks are supported to be downgraded to read locks, and read locks are not supported to be upgraded to write locks.

Code example:

public class LockTest {

    private static ReentrantReadWriteLock lock = new ReentrantReadWriteLock(false);
    private static ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
    private static ReentrantReadWriteLock.ReadLock readLock = lock.readLock();

    public static void main(String[] args) {
        new Thread(() -> write()).start();
        new Thread(() -> read()).start();
    }

    private static void read() {
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "开始学习《Thinking in Java》");
            writeLock.lock();
            System.out.println(Thread.currentThread().getName() + "获得到了写锁");
        } finally {
            writeLock.unlock();
            System.out.println(Thread.currentThread().getName() + "太难了!我不学了!");
            readLock.unlock();
        }
    }

    private static void write() {
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "开始印刷《Thinking in Java》");
            readLock.lock();
            System.out.println(Thread.currentThread().getName() + "在写锁中获取到了读锁");
        } finally {
            readLock.unlock();
            System.out.println(Thread.currentThread().getName() + "印刷完成");
            writeLock.unlock();
        }
    }
}

operation result:

Thread-0开始印刷《Thinking in Java》
Thread-0在写锁中获取到了读锁
Thread-0印刷完成
Thread-1开始学习《Thinking in Java》

We can see that the read lock is successfully obtained in the write lock, but it is always blocked read lock. Note that lock upgrade is not supported!

Why ReentrantReadWriteLock does not support lock escalation

The main purpose is to avoid deadlock. For example, two threads A and B are reading, A upgrade requires B to release the read lock, B upgrade requires A to release the read lock, and wait for each other to form an infinite loop. It's okay if you can strictly guarantee that only one thread is upgraded at a time.

It is reflected in the tryAcquireShared method and fullTryAcquireShared, such as the following judgment:

if (exclusiveCount(c) != 0) {
    if (getExclusiveOwnerThread() != current)
        return -1;

The meaning of the code is: when the write lock is held, if the thread holding the lock is not the current thread, it will return the failure to acquire the lock, otherwise it will continue to acquire the read lock. Call it lock downgrade.

Summary

  1. Features of read-write locks: read locks are shared locks, write locks are exclusive locks, and read locks and write locks cannot exist at the same time
  2. Queue-cutting strategy: In order to prevent thread starvation, read locks cannot be queued
  3. Upgrade strategy: only downgrade, not upgrade
  4. ReentrantReadWriteLock is suitable for reads more and writes less, which can improve concurrency efficiency, while ReentrantLock is suitable for ordinary occasions

Summarize

总结

ReentrantReadWriteLock cleverly uses the high 16 bits of the state value of AQS to indicate the number of read locks acquired, and the low 16 bits to indicate the reentrant times of the thread that acquired the write lock, and operates on it through CAS to achieve read-write separation, which is very It is suitable for use in scenarios with more reading and less writing.


神秘杰克
765 声望383 粉丝

Be a good developer.