Read-write locks Java Concurrency notes principle of ReentrantReadWriteLock 1.8

ReentrantLock is reentrant lock exclusive lock, exclusive lock at the same time only one thread can access, but in most scenarios, most of the time to provide reading services, and write less service time occupied. However, there is no data read service competition, if a thread ban other threads read when reading is bound to result in reduced performance. So it provides a read-write lock. Read-write locks maintain a pair of lock, a lock and a read-write lock. By separating read and write locks, making concurrency locks have been greatly improved than the average row: at the same time allows multiple threads simultaneously read access, but access when writing thread, all threads read and write threads will be obstruction. 

The main characteristics of the read-write lock:

  1. Fairness: for fair and non fairness.
  2. Reentrancy: support for re-entry. Read-Write Lock supports up to 65535 and 65535 recursive write locks recursively read lock.
  3. Lock downgrade: Follow obtain a write lock, in order to obtain a read lock release write lock, write lock to be able to demote a read lock

ReadWriteLock write locks ReentrantReadWriteLock implement the interface, this interface maintains a pair of associated locks, one for the read-only operations, one for writing. As long as no writer, read locks can be maintained simultaneously by multiple reader threads. Write lock is exclusive.

public interface ReadWriteLock {
    Lock readLock();
    Lock writeLock();
}
复制代码

ReadWriteLock defines two methods. readLock () Returns the read operation for the lock, writeLock () returns the lock for a write operation. ReentrantReadWriteLock 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 and ReentrantLock as its lock body remains Sync, its read locks, write locks are relying Sync to achieve. So ReentrantReadWriteLock in fact not the same as there is only one lock, just get a read lock and write lock mode only, read-write locks it is actually two categories: ReadLock, writeLock, these two classes are to achieve lock 

Use of an int type in ReentrantLock state to represent synchronization state, the value represents the number of repetitions of a lock is acquired threads. However, the internal read-write lock ReentrantReadWriteLock maintains two pair of locking, with the need to maintain a plurality of state variable. Therefore, the lock write a "bitwise using cleavage" way to maintain this variable, we split it into two parts, 16 is a high reading, 16 is a low write. After the split, read-write locks is how quickly determine the status of read and write locks it? Through to operation. If the current synchronization state is S, then the write state is equal to S & 0x0000FFFF (high to erase the entire 16-bit), a read state is equal to S >>> 16 (0 complement unsigned 16-bit right shift). code show as below:

// 读锁同步状态占用的位数
static final int SHARED_SHIFT   = 16;
// 每次增加读锁同步状态,就相当于增加SHARED_UNIT
static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
// 读锁或写锁的最大请求数量(包含重入)
static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
// 低16位的MASK,用来计算写锁的同步状态
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; }

复制代码

That is, read and write locks to get and set the state as follows:

  • Acquiring a read lock state: S >>> 16
  • Increasing the read lock state: S + (1 << 16) namely: S + SHARED_UNIT
  • Write lock state acquisition: S & 0x0000FFFF
  • Increasing the write lock state: S + 1

Write lock

Write lock is a support reentrant exclusive lock.

Write lock acquisition

Write lock acquisition eventually calls tryAcquire (int arg), the method is implemented within the class in Sync:

 protected final boolean tryAcquire(int acquires) {
        Thread current = Thread.currentThread();
        //当前锁个数
        int c = getState();
        //写锁
        int w = exclusiveCount(c);
        if (c != 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方法:

// NonfairSync 非公平锁,不用阻塞,直接获取锁
final boolean writerShouldBlock() {
    return false; 
}

// FairSync 公平锁,按顺序来
final boolean writerShouldBlock() {
    return hasQueuedPredecessors();
} 
public final boolean hasQueuedPredecessors() {
    Node t = tail;
    Node h = head;
    Node s;
    return h != t && 
        ((s = h.next) == null || s.thread != Thread.currentThread()); //队列前面存在其他线程
}      
复制代码

The method and the ReentrantLock tryAcquire (int arg) is substantially the same, it is determined reentrant adds a condition: a read lock exists. Because you want to make sure that the read write lock operation locks are visible, if allowed to acquire a write lock in the presence of a read lock, then other threads that have been acquired read lock might not perceive the current write operation thread. So after reading the only other fully release the lock, write lock can only be acquired by the current thread, once the write lock acquired, all other read, write threads will be blocked 

Write lock release

Get a write lock runs out you need to release, WriteLock provides unlock () method releases 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;
    }
复制代码

Write lock release will eventually call the template method of release AQS (int arg) method first calls tryRelease (int arg) method attempts to release the lock, tryRelease (int arg) method for the read-write lock Sync inner class is defined, 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;
    }

复制代码

Written release lock and the exclusive lock ReentrantLock whole process is similar to the release of each state are reducing write, write status is 0 for a write lock has been completely released, so that other threads waiting to read and write locks can continue to access, acquire synchronization state, while the write thread changes visible to follow the thread.

Read lock

A read lock is reentrant shared lock, which can be held by multiple threads simultaneously, when no other threads write access, read or locks always achieve success.

Read lock acquisition

May acquire a read lock by lock ReadLock () method:

   public void lock() {
            sync.acquireShared(1);
   }
复制代码

The Sync acquireShared (int arg) is defined in the AQS:

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

tryAcqurireShared (int arg) tries to acquire the synchronization status read, the method used to acquire synchronization shared state, returns to succeed> = 0 returns the result, otherwise <0 returns the result.

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):cas设置读取锁状态
         */
        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) 
            //cachedHoldCounter为当前线程
            //为0说明之前线程获取过读锁,后面释放了,这个分支是为了对象复用,
            //不用重新创建对象了,因为后面释放锁会调用readHolds.remove();复制代码
                    readHolds.set(rh); 
                rh.count++;
            }
            return 1;
        }
        return fullTryAcquireShared(current);
    }

复制代码

readerShouldBlock方法:

  /**
   * 非公平锁的读锁获取策略
  */
  final boolean readerShouldBlock() {
        //为了防止写线程饥饿
        //如果同步队列中的第一个线程是以独占模式获取锁(写锁),
        //那么当前获取读锁的线程需要阻塞,让队列中的第一个线程先执行
        return apparentlyFirstQueuedIsExclusive();
  }

  /**
   *  公平锁的读锁获取策略
  */  
  final boolean readerShouldBlock() {
         //如果当前线程不是同步队列头结点的next节点(head.next) 
         //则阻塞当前线程 
        return hasQueuedPredecessors();
  }
final boolean apparentlyFirstQueuedIsExclusive() {
    Node h, s;
    return (h = head) != null &&
    (s = h.next)  != null &&
    !s.isShared() && s.thread != null;
}
复制代码

During a read lock acquisition slightly more complicated in terms of an exclusive lock with respect to, the entire process is as follows:  

  1. Because there is a lock downgrade, if the write lock and the lock holder is not the current thread exists a direct return failure, or continue 
  2. Based on the principle of fairness, to determine whether a read lock needs to block, the number of threads read locks held less than the maximum value (65535), and to set the lock state, if you run the following code (for HoldCounter then explained below), and returns 1. If not change the conditions, execution fullTryAcquireShared (). 

// 在读锁只被一个线程持有的情况下使用
private transient Thread firstReader = null;
private transient int firstReaderHoldCount;

// 最近(后)一个成功持有[读锁]的线程计数器。
private transient HoldCounter cachedHoldCounter;

// 当前线程的线程计数器
private transient ThreadLocalHoldCounter readHolds;

 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) {
                        // assert firstReaderHoldCount > 0;
                        //当前线程就是第一个获取读锁的线程,那么此时当然是重入锁。
                    } else {
                        if (rh == null) {
                            rh = cachedHoldCounter;
                            if (rh == null || rh.tid != current.getId()) {
                                rh = readHolds.get();
                                if (rh.count == 0)
                                    //线程阻塞之前,清空readHolds
                                    readHolds.remove();
                            }
                        }
                        if (rh.count == 0)
                            //当前线程的锁计数器为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;
            }
        }
    }

复制代码

fullTryAcquireShared (Thread current) based on "whether blocked waiting", "shared read lock count exceeds a limit," and the like are processed. If not blocked waiting, and a shared lock count does not exceed the limit, then the attempt to acquire the lock by CAS, and returns 1

Read lock release

The same as the write locks, read locks also provide unlock () read lock release:

  public void unlock() {
            sync.releaseShared(1);
  }
复制代码

Internal () method using Sync unlcok of releaseShared (int arg) method, which is defined in the AQS:

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

复制代码

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

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

During a read lock acquire and release locks, we always see a variable rh (HoldCounter), this variable plays a very important role in the read lock.  

We interpret the internal mechanism of the lock is actually a shared lock, in order to better understand HoldCounter, we forget that the probability that it is not a lock, but the equivalent of a counter. A shared lock operation is equivalent to the operation of the counter. Acquire a shared lock, the counter + 1, shared locks are released, the counter --1. Only when the shared lock can be released after thread acquires a shared lock, re-entry operation. So the role of HoldCounter is the current thread holds a number of shared locks, this number must be bound together with thread, otherwise the operation lock other threads will throw an exception. We look at the definition of HoldCounter:

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

HoldCounter definition is very simple, it is a counter count and thread id tid two variables. According to this mean we need to see HoldCounter and bind to a thread, and we want to know if an object and thread binding only have tid is not enough, but also from the above code, we can see just recorded HoldCounter tid, can hardly be the role of the binding thread. So how to achieve it? The answer is ThreadLocal, defined as follows:

      // 本地线程计数器static final class ThreadLocalHoldCounter
    extends ThreadLocal<HoldCounter> {
    // 重写初始化方法,在没有进行set的情况下,获取的都是该HoldCounter值
    public HoldCounter initialValue() {
        return new HoldCounter();
    }复制代码

You can be bound by the code above HoldCounter and thread. Therefore, HoldCounter should be is a counter on the binding thread, and the thread is bound ThradLocalHoldCounter ThreadLocal. From the above we can see ThreadLocal will HoldCounter bound to the current thread, while HoldCounter also holds the thread Id, so in order to know when to release the lock on the inside ReadWriteLock cache read a thread (cachedHoldCounter) whether the current thread. The advantage of this is that the number can be reduced ThreadLocal.get (), because this is a time-consuming operation. It should be noted that the reason for such HoldCounter binding thread id without binding thread object is to avoid HoldCounter and ThreadLocal binding each other GC difficult to release them (although GC can intelligently find this reference recover them, but this will take some costs), so in fact we do this just to help GC to quickly recover objects only. 

 See here we understand HoldCounter role, we look at a snippet read lock acquisition: 

  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
                }

复制代码

This code involves several variables: firstReader, firstReaderHoldCount, cachedHoldCounter. Let's clear rationale of these variables:

private transient Thread firstReader = null;
private transient int firstReaderHoldCount;
private transient HoldCounter cachedHoldCounter;
复制代码

firstReader look at the name to understand for the first acquire a read lock threads, firstReaderHoldCount to acquire a read lock on the number of re-entry, cachedHoldCounter to the cache HoldCounter.

 Reason clearly all the above variables, HoldCounter understand, we come to the part of the code indicated above comments, as follows:

  //如果获取读锁的线程为第一次获取读锁的线程,则firstReaderHoldCount重入数 + 1
    else if (firstReader == current) {
        firstReaderHoldCount++;
    } else {
        //非firstReader计数
        if (rh == null)
            rh = cachedHoldCounter;
        //rh == null 或者 rh.tid != current.getId(),需要获取rh
        if (rh == null || rh.tid != getThreadId(current))
            rh = readHolds.get();
            //加入到readHolds中
        else if (rh.count == 0)
            readHolds.set(rh);
        //计数+1
        rh.count++;
        cachedHoldCounter = rh; // cache for release
    }

复制代码

Here explain why the introduction of firstRead, firstReaderHoldCount. This is to a question of efficiency, firstReader is not put into readHolds the case if only a read lock will prevent find readHolds.

Lock downgrade

The opening is on the LZ describes the read-write lock has a feature that degrade lock, lock downgrade would mean a write lock is downgraded to a read lock, but you need to follow to obtain a write lock, in order to obtain a read lock release write lock. Note that if the current thread to obtain a write lock, then release the write lock, and then acquire a read lock this process can not be called a lock downgrade, degrade lock must follow that order.  

A method of acquiring a read lock tryAcquireShared (int unused), the piece of code is to interpret lock degradation:

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

复制代码

Downgrade lock release read lock acquisition is necessary? It is certainly necessary. Just think, if the current thread A does not acquire a read lock but released directly write lock, this time to another thread B acquiring a write lock, then thread B data changes are not visible on the current thread A. If you get a read lock, then thread B to determine if there is a read lock has not released it will be blocked in the process of acquiring a write lock, only after the current thread A release read lock, thread B will acquire a write lock success. 

Lock downgrade Example:


class CachedData {
   Object data;
   volatile boolean cacheValid; //缓存有效标志位
   final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
 
   void processCachedData() {
    //先加读锁取读
     rwl.readLock().lock();
     if (!cacheValid) {
       // Must release read lock before acquiring write lock
       rwl.readLock().unlock();
        //缓存无效,加写锁修改
       rwl.writeLock().lock();
       try {
         if (!cacheValid) { // Recheck 

           data = ...
           cacheValid = true;
         }
         // 获取读锁
         rwl.readLock().lock();
       } finally {
         rwl.writeLock().unlock(); // 写锁释放,持有读锁,完成锁降级
       }
     }
 
     try {
       use(data);
     } finally {
       rwl.readLock().unlock();
     }
   }
复制代码

The above code, when the data is changed, update the variable (Boolean volatile type) is set to false, this time all the threads that access the processData () methods are not able to perceive a change, but only one thread can obtain a write lock, other threads will be blocked on read and write locks the lock () method. After the current thread acquires a write lock finishes preparing the data, and then acquire a read lock, then release the write lock, complete lock downgrade.


Third, the summary

By source code analysis above, we can see a phenomenon:

In the case of the thread holds a read lock, the thread can not obtain a write lock (write lock because the acquisition time, if we find the current read lock is occupied, immediately get fail, regardless of read lock is not held by the current thread).

In the case of thread holds the write lock, the thread can continue to acquire a read lock (read lock when acquiring a write lock on the case if it is found to be occupied only write locks are not occupied by the current thread will get failure).

Think about this design is reasonable: because when threads from acquiring a read lock when there may be other threads at the same time also hold the lock and therefore can not get a read lock thread "upgrade" to write locks; and for obtaining written lock thread, it must be the exclusive write locks, so it can continue to get a read lock, when it also acquired write lock and the read lock, write lock can also be released to continue to hold a read lock, so that a write lock on "downgrade" to read lock.

In summary:

To write a thread while holding locks and read locks, you must first obtain a write lock and then get a read lock; write lock can "downgrade" is a read lock; read lock can not "upgrade" to write locks.

Reference material

https://blog.csdn.net/chenssy/article/details/68059443


Guess you like

Origin juejin.im/post/5ced148b51882520724c7898