java并发系列:ReentrantReadWriteLock读写锁

一 序

     之前整理过AQS(AbstractQueuedSynchronizer 锁的基础。 还有重入锁(ReentrantLock),Java常见的多是排他锁,这些锁在同一时刻只允许一个线程进行访问,而读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升。

  读写锁能有效提高读比写多的场景下的程序性能,比排它锁好。

  对应的特性:公平性选择,重进入(读写锁都支持),锁降级。

  本文主要是基于《Java并发编程艺术》来整理的,有差别的两点:firstReader 还有读写锁降级。书上出于篇幅一笔带过的地方,后面加上我自己的理解。所以主要思路是:写锁的获取释放,读锁的获取释放,锁降级。

二 读写锁的接口及类

    ReadWriteLock仅定义了获取读锁和写锁的两个方法,即readLock()和writeLock()方法

public interface ReadWriteLock {  
    /** 
     * Returns the lock used for reading. 
     * 
     * @return the lock used for reading. 
     */  
    Lock readLock();  
  
    /** 
     * Returns the lock used for writing. 
     * 
     * @return the lock used for writing. 
     */  
    Lock writeLock();  
}  
public class ReentrantReadWriteLock
        implements ReadWriteLock, java.io.Serializable {
    private static final long serialVersionUID = -6992448646407690164L;
    /** Inner class providing readlock */
    private final ReentrantReadWriteLock.ReadLock readerLock;
    /** Inner class providing writelock */
    private final ReentrantReadWriteLock.WriteLock writerLock;
    /** Performs all synchronization mechanics */
    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 {}

    static final class NonfairSync extends Sync {}

    static final class FairSync extends Sync {}

    public static class ReadLock implements Lock, java.io.Serializable {}

    public static class WriteLock implements Lock, java.io.Serializable {}
} 

读写锁内部类关系如下图所示

abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 6317671515068378041L;

Sync抽象类继承自AQS抽象类,Sync类提供了对ReentrantReadWriteLock的支持。

Sync类的属性如下:

abstract static class Sync extends AbstractQueuedSynchronizer {
    // 版本序列号
    private static final long serialVersionUID = 6317671515068378041L;        
    // 高16位为读锁,低16位为写锁
    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;
    // 本地线程计数器
    private transient ThreadLocalHoldCounter readHolds;
    // 缓存的计数器
    private transient HoldCounter cachedHoldCounter;
    // 第一个读线程
    private transient Thread firstReader = null;
    // 第一个读线程的计数
    private transient int firstReaderHoldCount;
}

其中三部分:1 高低位状态(后面写),2. 内部类HoldCounter主要与读锁配套使用。内部类ThreadLocalHoldCounter

  /**
         * ThreadLocal subclass. Easiest to explicitly define for sake
         * of deserialization mechanics.
         */
        static final class ThreadLocalHoldCounter
            extends ThreadLocal<HoldCounter> {
            public HoldCounter initialValue() {
                return new HoldCounter();
            }
        }

重写了ThreadLocal的initialValue方法,ThreadLocal类可以将线程与对象相关联。没有set方法,直接get初始化后的holdcounter.

   ReentrantReadWriteLock,除了接口方法之外,还提供了一些便于外界监控其内部工作状态的方法。如下所示

方法名称 描述
int getReadLockCount() 返回当前读锁被获取的次数。该次数不等于获取读锁的线程数,比如:仅一个线程,它连续获取(重进入)了n次读锁,那么占据读锁的线程数是1,但该方法返回n
int getReadHoldCount() 返回当前线程获取读锁的次数。该方法在Java 6 中加入到ReentrantReadWriteLock中,使用ThreadLocal保存当前线程获取的次数,这也使得Java 6 的实现变得更加复杂
boolean isWriteLocked() 判断写锁是否被获取
int getWriteHoldCount() 返回当前写锁被获取的次数

也是依赖上面提到的Sync实现的。举例

 final int getReadLockCount() {
            return sharedCount(getState());
        }
        final boolean isWriteLocked() {
            return exclusiveCount(getState()) != 0;
        }

接下看读写锁的实现设计。

三 读写状态设计

      读写锁同样依赖自定义同步器(AQS)来实现同步功能,而读写状态就是其同步器的同步状态。回想ReentrantLock中自定义同步器的实现,同步状态表示锁被一个线程重复获取的次数,而读写锁的自定义同步器需要在同步状态(一个整型变量)上维护多个读线程和一个写线程的状态.怎么办啊?简单理解就是“掰开”用。由于读锁可以同时有多个,肯定不能再用辦成两份用的方法来处理了,但我们有 ThreadLocal,可以把线程重入读锁的次数作为值存在 ThreadLocal 里,就是上面sync类的HoldCounter。

    下面是《并发编程的艺术》给出的术语:如果在一个整型变量上维护多种状态,就一定需要“按位切割使用”这个变量,读写锁是将变量切分成了两个部分,高16位表示读,低16位表示写,划分方式如图所示。

 

        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;

假设当前同步状态值为S,get和set的操作如下:

(1)获取写状态:
    S&0x0000FFFF:将高16位全部抹去
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
(2)获取读状态:
    S>>>16:无符号补0,右移16位
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
(3)写状态加1:
     S+1    tryAcquire方法里面
    // Reentrant acquire
                setState(c + acquires);
(4)读状态加1:
  S+(1<<16)即S + 0x00010000
在代码层的判断中,如果S不等于0,当写状态(S&0x0000FFFF),而读状态(S>>>16)大于0,则表示该读写锁的读锁已被获取。

四 写锁的获取与释放

      写锁是一个支持重进入的排它锁。如果当前线程已经获取了写锁,则增加写状态。如果当前线程在获取写锁时,读锁已经被获取(读状态不为0)或者该线程不是已经获取写锁的线程,则当前线程进入等待状态,获取写锁的代码如下

public void lock() {
    sync.acquire(1);
}
public void unlock() {
    sync.release(1);
}
到就是调用的独占式同步状态的获取与释放,因此真实的实现就是Sync的 tryAcquire和 tryRelease。
 protected final boolean tryAcquire(int acquires) {

            Thread current = Thread.currentThread();
            int c = getState();//aqs
            // 用 state & 65535 得到低 16 位的值
            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    将 state  + 1             
                setState(c + acquires);
                return true;
            }
            //当需要判断公平锁(非公平情况下,返回 false)或者CAS设置state失败,需要返回false
            if (writerShouldBlock() || !compareAndSetState(c, c + acquires))
                return false;
             // 修改成功 state 后,修改锁的持有线程。    
            setExclusiveOwnerThread(current);
            return true;
        }
         该方法除了重入条件(当前线程为获取了写锁的线程)之外,增加了一个读锁是否存在的判断。如果存在读锁,则写锁不能被获取,原因在于:读写锁要确保写锁的操作对读锁可见,如果允许读锁在已被获取的情况下对写锁的获取,那么正在运行的其他读线程就无法感知到当前写线程的操作。因此只有等待其他读线程都释放了读锁,写锁才能被当前线程所获取,而写锁一旦被获取,则其他读写线程的后续访问均被阻塞。

        写锁的释放比较简单。 每次释放均减少写状态,当写状态为0时表示写锁已被释放,从而等待的读写线程能够继续访问读写锁,同时前次写线程的修改对后续读写线程可见。    

protected final boolean tryRelease(int releases) {
    // 是否持有当前锁
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    // 计算 state 值
    int nextc = getState() - releases;
    // 计算写锁的状态,如果是0,说明是否成功。
    boolean free = exclusiveCount(nextc) == 0;
    // 释放成功,设置持有锁的线程为 null。
    if (free)
        setExclusiveOwnerThread(null);
    // 设置 state
    setState(nextc);
    return free;
}

五  读锁的获取与释放

      读锁是一个支持重进入的共享锁,它能够被多个线程同时获取,在没有其他写线程访问(或者写状态为0)时,读锁总会成功的被获取,而所做的也只是(线程安全的)增加读状态。如果当前线程已经获取了读锁,则增加读状态。如果当前线程在获取读锁时,写锁已被其他线程获取,则进入等待状态。

       类似于写锁,读锁的lock和unlock的实际实现对应Sync的 tryAcquireShared 和 tryReleaseShared方法。不同的事,因为考虑到获取读锁次数总和等,代码实现起来比写锁复杂。       

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);
            /* 判断读锁是否应该被阻塞
             * readerShouldBlock():读锁是否需要等待(公平锁原则)
	           * r < MAX_COUNT:持有线程小于最大数(65535)
             * compareAndSetState(c, c + SHARED_UNIT):设置读取锁状态
            */
            if (!readerShouldBlock() &&
                r < MAX_COUNT &&
                compareAndSetState(c, c + SHARED_UNIT)) {
                //如果是第一个获取读状态的线程,第一个读锁firstRead是不会加入到readHolds中
                if (r == 0) {
                     // 设置第一个读线程
                    firstReader = current;
                    // 读线程占用的资源数为1
                    firstReaderHoldCount = 1;
               // 当前线程为第一个读线程,表示第一个读锁线程重入    
                } else if (firstReader == current) { 
                   //第一个线程对应获取的读锁数量加1               
                    firstReaderHoldCount++;
                } else {
                   //是别的线程
                   // cachedHoldCounter 代表的是最后一个获取读锁的线程的计数器。
                    HoldCounter rh = cachedHoldCounter;
                    //如果最后一个线程计数器是 null 或者不是当前线程,那么就新建一个 HoldCounter 对象
                    if (rh == null || rh.tid != getThreadId(current))
                        //获取当前线程的计数器
                        cachedHoldCounter = rh = readHolds.get();
                    else if (rh.count == 0)// 计数为0
                         //加入到readHolds中
                        readHolds.set(rh);
                        //计数+1
                    rh.count++; 
                }
                return 1;
            }
            // 获取读锁失败,放到循环里重试。
            return fullTryAcquireShared(current);
        }
final int fullTryAcquireShared(Thread current) {

    HoldCounter rh = null;
    for (;;) { // 自旋
        // 获取状态
        int c = getState();
        if (exclusiveCount(c) != 0) { // 写线程数量不为0
            if (getExclusiveOwnerThread() != current) // 不为当前线程
                return -1;
        } else if (readerShouldBlock()) { // 写线程数量为0并且读线程被阻塞
            // 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)) { // 计数器为空或者计数器的tid不为当前正在运行的线程的tid
                        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)) { // 比较并且设置成功,尝试读锁高位+1
            if (sharedCount(c) == 0) { // 读线程数量为0
                // 设置第一个读线程
                firstReader = current;
                // 计数器1
                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 与tryAcquireShared逻辑类似,就是通过自旋+CAS获取锁。补充一个图更清晰些


  读锁的释放(线程安全的,可能有多个读线程同时释放读锁)减少读状态,减少的值是(1 << 16)

protected final boolean tryReleaseShared(int unused) {
    // 获取当前线程
    Thread current = Thread.currentThread();
    if (firstReader == current) { // 当前线程为第一个读线程
          //如果第一个获取读锁的线程只获取了一个锁那么firstReader=null
        //否则firstReaderHoldCount--
        // assert firstReaderHoldCount > 0;
        if (firstReaderHoldCount == 1) 
            firstReader = null;
        else // 减少占用的资源
            firstReaderHoldCount--;
    } else { // 当前线程不为第一个读线程
        // 获取缓存的计数器
        HoldCounter rh = cachedHoldCounter;
        if (rh == null || rh.tid != getThreadId(current)) // 计数器为空或者计数器的tid不为当前正在运行的线程的tid
            // 获取当前线程对应的计数器
            rh = readHolds.get();
        // 获取计数
        int count = rh.count;
        if (count <= 1) { // 计数小于等于1
            // 当前线程获取的读锁小于等于1那么就将remove当前线程的HoldCounter移除
            readHolds.remove();
            if (count <= 0) // 计数小于等于0,抛出异常
                throw unmatchedUnlockException();
        }
        // 当前线程拥有的读锁数量减1
        --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;
    }
}
书上这里写的比较少。补充下:读锁获取释放里面相关的点
firstReader 是获取读锁的第一个线程。如果只有一个线程获取读锁,还不需要动用ThreadLocal,直接往firstReaderHoldCount这个成员变量里存重入数,当有第二个线程来的时候,就要动用ThreadLocal变量readHolds了,每个线程拥有自己的副本,用来保存自己的重入数。(使用这样一个变量速度更快)。
firstReaderHoldCount是 firstReader的计数器。同上。
cachedHoldCounter是最后一个获取到读锁的线程计数器,每当有新的线程获取到读锁,这个变量都会更新。这个变量的目的是:当最后一个获取读锁的线程重复获取读锁,或者释放读锁,就会直接使用这个变量,速度更快,相当于缓存。

六 锁降级

    先说下概念:重入还允许从写入锁降级为读取锁,其实现方式是:先获取写入锁,然后获取读取锁,最后释放写入锁(书上用了把持住当前拥有的写锁,比较形象)。但是,从读取锁升级到写入锁是不可能的。

  官网api:https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/locks/ReentrantReadWriteLock.html

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 {
         // Recheck state because another thread might have
         // acquired write lock and changed state before we did.
         if (!cacheValid) {
           data = ...
           cacheValid = true;
         }
         // Downgrade by acquiring read lock before releasing write lock
         rwl.readLock().lock();
       } finally {
         rwl.writeLock().unlock(); // Unlock write, still hold read
       }
     }

     try {
       use(data);
     } finally {
       rwl.readLock().unlock();
     }
   }
 }
        上述示例中,当数据发生变更后,update变量(布尔类型且volatile修饰)被设置为false,此时所有访问processData()方法的线程都能够感知到变化,但只有一个线程能够获取到写锁,而其他线程会被阻塞在读锁和写锁的lock()方法上。当前程获取写锁完成数据准备之后,再获取读锁,随后释放写锁,完成锁降级。

     书上说的“锁降级中读锁的获取是否必要呢?答案是必要的。主要原因是保证数据的可见性,如果当前线程不获取读锁而是直接释放写锁,假设此刻另一个线程(记作线程T)获取了写锁并修改了数据,则当前线程无法感知线程T的数据更新。如果当前线程获取读锁,即遵循锁降级的步骤,则线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程T才能获取写锁进行数据更新

     这里我认为是容易有歧义的,首先我得承认大神很牛,《并发编程的艺术》是本好书,在聊聊并发上我就看了,受益匪浅。

我理解如下:

  关于“那么当前线程无法感知线程T的数据更新”我理解,即使是先释放写锁,然后获取读锁也没有问题,只不过会可能会被其他线程的写锁阻塞一段时间(阻塞期间无法感知,但最终获取的数据不是脏数据)。但是并不意味着,随后的这个读操作看不到之前别的线程的写锁下的写操作,只要写锁被释放数据更新还是可以看到的,最终数据是一致的。
   如果考虑文章一开头介绍的读写锁适应场景,一般使用锁降级的前提是读优先于写,如果当新线程请求读锁的时候,当前持有写锁的线程需要马上进行降级,保证所有读锁的顺利获取,阻塞后续写锁。
   所以猜测锁降级目的或许是为了减少线程的阻塞唤醒。明显当不使用锁降级,线程2修改数据时,线程1自然要被阻塞,而使用锁降级时则不会。感知”其实是想强调读的实时连续性,但是容易让人误导为强调数据操作,锁降级和“可见性”没有太多关系。

  ****************************

一个线程要想同时持有写锁和读锁,必须先获取写锁再获取读锁;写锁可以“降级”为读锁;读锁不能“升级”为写锁

参考:http://ifeve.com/java-art-reentrantlock/

猜你喜欢

转载自blog.csdn.net/bohu83/article/details/80764781