头图

After modifying the method with the synchronize keyword, only one thread is allowed to access it. Although this is beneficial to ensure data security, it is contrary to the actual scenario. In practice, data is read more and written less, we need more coarse and fine-grained concurrent locks. The JVM concurrent.locks package provides us with the ReadWriteLock read-write lock, with two built-in locks, a read lock and a write lock, to satisfy multiple threads to read data concurrently, and to mutually exclude all threads when writing, which not only ensures data security, but also improves the response volume.

concept

Read lock: Can be understood as a shared lock, allowing multiple threads to read at the same time Write lock: Exclusive lock, only one thread is allowed to access read-write mutual exclusion: When acquiring a write lock, you must wait for all read locks to be released before acquiring On success, the read lock will block the write lock, and the write lock will block all threads.
Lock escalation: When using a read lock, the thread that has acquired the read lock acquires the write lock without releasing the read lock, which is lock escalation. This is not allowed and lock escalation can cause deadlock.

 // 这个会造成死锁
        ReadWriteLock  lock = new ReentrantReadWriteLock();
        lock.readLock().lock();
        lock.writeLock().lock();

Lock downgrade: The thread that has acquired the write lock is allowed to acquire the read lock without releasing the lock. It is worth noting that the read lock and the write lock still need to be released separately.

 //并不会造成死锁
        ReadWriteLock lock = new ReentrantReadWriteLock();
        lock.writeLock().lock();
        lock.readLock().lock();

Use the official example to demonstrate the usage scenario of ReentrantReadWriteLock. Every time the cache is obtained, first determine whether the cache has expired, and if so, use the write lock to update the cache.

 class CachedData {
    Object data;
    volatile boolean cacheValid; //缓存失效标记
    final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
      void processCachedData() {
      rwl.readLock().lock(); 
     if (!cacheValid) {
        // 必须释放了读锁才能去获取写锁,这样不会造成死锁
        rwl.readLock().unlock(); 
       rwl.writeLock().lock(); 
       try {
          // 双重检查状态,因为在获取锁的可能被其他线程更新状态了
          // 获取到写锁,更新缓存和状态
          if (!cacheValid) {
            data = ...
            cacheValid = true;
          }
          // 通过在释放写锁之前获取读锁来降级
          rwl.readLock().lock(); 
       } finally {
          rwl.writeLock().unlock();
 // Unlock write, still hold read 
       } 
     } 
       try {
        use(data); 
     } finally {
        rwl.readLock().unlock();
      } 
   } 
 }

The code is very small, but it is very representative, which is very suitable for caching such scenarios where there are many reads and few updates. Every time you read the cache, first open the read lock, check the cache status, and need to update the cache. Release the read lock first and then acquire the write lock. Before updating, determine that the cache has not been updated by other threads. After updating the data, downgrade to the read lock, release the write lock, and use the cache to release the read lock.

Source code analysis

The source code analysis here only briefly explains the acquisition and release principles of the two locks. Before reading the source code, prepare your own AQS knowledge points.
ReentrantReadWriteLock is an implementation class that implements the ReadWriteLock interface. Internally, AQS int state is used to indicate the status of the read-write lock
state.png
As shown in the figure above, the acquisition and release of the two locks are performed simultaneously using int state . The lower 16 bits represent the number of write lock acquisitions, and the upper 16 bits represent the number of read lock acquisitions. Use the inner class Sync to separately write the specific implementation of the acquisition and release of shared locks and exclusive locks, and then use ReadLock and WriteLock to call the methods of shared locks and exclusive locks respectively. The source code reading starts with the Sync inner class.

Internal properties

 abstract static class Sync extends AbstractQueuedSynchronizer {
         //读 写 锁分界点
        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; }

 //主要用于保存每一个读锁线程的重入次数
 static final class HoldCounter { //初始化对象,就将当前线程id赋值给tid
            int count = 0; //重入次数
            // Use id, not reference, to avoid garbage retention
            final long tid = getThreadId(Thread.currentThread());
        }

        /**
         *  保存HoldCounter到每一个线程私有栈祯
         */
        static final class ThreadLocalHoldCounter
            extends ThreadLocal<HoldCounter> {
            public HoldCounter initialValue() {  //实现初始化接口,每一次调用get()时,没有值就会调用初始化方法
                return new HoldCounter();
            }
        }

        /**
         *  记录读锁重入次数
         */
        private transient ThreadLocalHoldCounter readHolds;

        /**
         *  这个是上一个读锁HoldCounter 缓存
         */
        private transient HoldCounter cachedHoldCounter;

I think here that the read lock is a shared lock on the number of re-entries, and the state cannot accurately express how many times each thread has re-entered, so HoldCounter needs to be used to record the number of times each thread acquires the lock. When releasing the lock, Will see how to use it.

Acquiring and releasing shared locks

 protected final int tryAcquireShared(int unused) {
            Thread current = Thread.currentThread();
            int c = getState();
            // 当独占锁不等于0,这时只有独占锁是自身的情况下才能获取到读锁
            //两个条件都满足时,写锁获取到读锁   锁降级
            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 必须是读锁线程
                    firstReader = current;
                    firstReaderHoldCount = 1;
                } else if (firstReader == current) { //重入
                    firstReaderHoldCount++;
                } else { //当前线程不是首个读锁持有者,要使用HoldCounter 记录重入
                    HoldCounter rh = cachedHoldCounter;  //这是上一个线程缓存
                    if (rh == null || rh.tid != getThreadId(current))
                         //这里会返回当前线程初始化值 也就是数量为空0
                        //将当前线程重入对象赋值给缓存
                        cachedHoldCounter = rh = readHolds.get();
                    else if (rh.count == 0) //第一次进入
                        readHolds.set(rh);
                    rh.count++;
                }
                return 1;
            }
            // cas 竞争失败,完整版本共享锁获取
            return fullTryAcquireShared(current);
        }

readerShouldBlock is a queue blocking strategy method used to distinguish between fair locks and unfair locks. When it returns true, it will block all threads that acquire read locks.

 final int fullTryAcquireShared(Thread current) {
            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()) { //当返回true,则说明已经存在堵塞线程,这是要么自旋,要么失败
                    // 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)) { //高16位运算,获取共享锁成功
                    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;
                }
            }
        }

To summarize: When acquiring a shared lock, the acquire lock method will return failure immediately only when an exclusive lock is detected.
Look at the shared lock release

 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;  //共享锁数据为0
            }
        }

HoldCounter is used to maintain the number of locks released by each thread to ensure that the number of locks released will not exceed the number held by itself.

Exclusive lock acquisition and 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;
        }

        protected final boolean tryAcquire(int acquires) {
            Thread current = Thread.currentThread();
            int c = getState();
            int w = exclusiveCount(c);
            if (c != 0) { // c 不能等于0 ,当前仍然持有锁,有可能是独占锁或者是共享锁
                // 如果独占锁为空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;
        }

writerShouldBlock: When returning true, it will block the thread acquiring the lock, which is used to distinguish between fair locks and unfair lock implementations. Combined with the above code, when true is returned, the lock will not be acquired, and it will fail directly.
Exclusive 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;
        }

Fair and unfair locks

There are two locks to choose from inside ReentrantReadWriteLock, fair lock and unfair lock. The selection is made through construction parameters, and unfair locks are used by default.

 public ReentrantReadWriteLock() {
        this(false);
    }

    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; }

Unfair lock: When acquiring a read lock or a write lock, the thread acquiring the lock is not sequential. The thread in the blocking queue may wait for a long time and cannot acquire the lock, but the thread that does not wait in the blocking queue can acquire it quickly. lock, which will cause thread starvation, but will have higher throughput than fair locks.
Fair lock: Ensure that each thread that has been waiting the longest obtains the thread execution right first, and the thread will acquire the lock in the order of AQS blocking, which is beneficial to avoid thread starvation, but it is necessary to judge that the queue has a certain performance loss when acquiring the lock, so Throughput is not as high as unfair.

The difference between fair locks and unfair locks is that the writerShouldBlock and readerShouldBlock methods are implemented differently.
Fair lock implementation

 static final class FairSync extends Sync {
        private static final long serialVersionUID = -2274990926593161451L;
        final boolean writerShouldBlock() {
            return hasQueuedPredecessors();
        }
        final boolean readerShouldBlock() {
            return hasQueuedPredecessors();
        }
    }

hasQueuedPredecessors: Returning true means that there is a blocked thread in AQS. Only when a write lock occurs, the lock acquiring thread will be put into the queue, so readerShouldBlock will always return false when the read lock is acquired.
unfair lock

 static final class NonfairSync extends Sync {
        private static final long serialVersionUID = -8159625535654395037L;
        final boolean writerShouldBlock() {
            return false; // writers can always barge
        }
        final boolean readerShouldBlock() {
            // 只有堵塞队列第一个线程为非共享锁时才会返回true
            // 当队列前面已经出现写锁了,所有共享锁都不能和写锁竞争,放弃竞争
            return apparentlyFirstQueuedIsExclusive();
        }
    }

From the above code, we know that only these two methods return true, and neither can compete for locks. The strategy of fair locks is very obvious. As long as there are threads in the blocked queue, lock competition will be abandoned. Unfair locks are when writing locks, regardless of whether there are threads in the queue, they will try to compete. When writing locks, only the thread at the front of the queue will give up the competition. Generally speaking, fair locks and unfair lock logic Basically the same logic as ReentrantLock.

tryLock

In the objects of read locks and write locks, there is a tryLock method, which is different from the lock method. Although they all call the internal Sync method, the lock acquisition method is basically the same as the above analysis of tryAcquire and tryAcquireShared. , the only thing missing is the use of readerShouldBlock and writerShouldBlock. Use this method to acquire locks, regardless of fair locks or unfair locks, the lock acquisition logic is the same. Regardless of whether there are threads in the blocking queue, there will be direct competition to acquire the lock. In an unfair lock, the read lock will grant the first write lock in the queue, and the write lock will have a higher priority than the read lock. But tryLock does not exist. All lock competitions are fair and fast. It is understandable that this method will have a higher priority (compared to lock) in acquiring locks.


神易风
106 声望52 粉丝

alert("hello world")