JUC之深入理解ReentrantReadWriteLock

GitHub:https://github.com/JDawnF/learning_note

ReentrantReadWriteLock ,读写锁,是用来提升并发程序性能的锁分离技术的 Lock 实现类。可以用于 “多读少写” 的场景,读写锁支持多个读操作并发执行,写操作只能由一个线程来操作。

ReadWriteLock 使得你可以同时有多个读取者,只要它们都不试图写入即可。如果写锁已经被其他任务持有,那么任何读取者都不能访问,直至这个写锁被释放为止。

ReadWriteLock 对程序性能的提高主要受制于如下几个因素:

  1. 数据被读取的频率与被修改的频率相比较的结果。

  2. 读取和写入的时间

  3. 有多少线程竞争

  4. 是否在多处理机器上运行

1. 简介

重入锁 ReentrantLock 是排他锁,排他锁在同一时刻仅有一个线程可以进行访问,但是在大多数场景下,大部分时间都是提供读服务,而写服务占有的时间较少。然而,读服务不存在数据竞争问题,如果一个线程在读时禁止其他线程读势必会导致性能降低。所以就提供了读写锁。

读写锁维护着一对锁,一个读锁和一个写锁。通过分离读锁和写锁,使得并发性比一般的排他锁有了较大的提升:

  • 在同一时间,可以允许多个读线程同时访问。

  • 但是,在写线程访问时,所有读线程和写线程都会被阻塞。

读写锁的主要特性

  1. 公平性:支持公平性和非公平性。

  2. 重入性:支持重入。读写锁最多支持 65535 个递归写入锁和 65535 个递归读取锁。

  3. 锁降级:遵循获取写锁,再获取读锁,最后释放写锁的次序,如此写锁能够降级成为读锁。

2. ReadWriteLock

java.util.concurrent.locks.ReadWriteLock读写锁接口。定义方法如下:

 Lock readLock();
 Lock writeLock();
  • 一对方法,分别获得读锁和写锁 Lock 对象

3. ReentrantReadWriteLock

java.util.concurrent.locks.ReentrantReadWriteLock ,实现 ReadWriteLock 接口,可重入的读写锁实现类。在它内部,维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。只要没有 Writer 线程,读取锁可以由多个 Reader 线程同时保持。也就说,写锁是独占的,读锁是共享的。

ReentrantReadWriteLock 类的大体结构如下:

/** 内部类  读锁 */
 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);
 }
 ​
 /** 返回用于写入操作的锁 */
 @Override
 public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
 /** 返回用于读取操作的锁 */
 @Override
 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 与 ReentrantLock一样,其锁主体也是 Sync,它的读锁、写锁都是通过 Sync 来实现的。所以 ReentrantReadWriteLock 实际上只有一个锁,只是在获取读取锁和写入锁的方式上不一样。

  • 它的读写锁对应两个类:ReadLock 和 WriteLock 。这两个类都是 Lock 的子类实现。

读写锁同步状态的设计

在 ReentrantLock 中,使用 Sync ( 实际是 AQS )的 int 类型的 state 来表示同步状态,表示锁被一个线程重复获取的次数。使用的是AQS中的一个同步状态state表示 当前共享资源是否被其他线程锁占用。如果为0则表示未被占用,其他值表示该锁被重 入的次数。但是,读写锁 ReentrantReadWriteLock 内部维护着一对读写锁,如果要用一个变量维护多种状态,需要采用“按位切割使用”的方式来维护这个变量,将其切分为两部分:高16为表示读,低16为表示写。

分割之后,读写锁通过位运算迅速确定读锁和写锁的状态。假如当前同步状态为S,那么:

  • 写状态,等于 S & 0x0000FFFF(将高 16 位全部抹去)

  • 读状态,等于 S >>> 16 (无符号补 0 右移 16 位)。

代码如下:

// Sync.java
 ​
 static final int SHARED_SHIFT   = 16; // 位数
 static final int SHARED_UNIT    = (1 << SHARED_SHIFT);  //左移n位相当于乘以2的n次方
 static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1; // 每个锁的最大重入次数,65535
 static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
 ​
 static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
 // &表示两个数都转为二进制,然后从高位开始比较,如果两个数都为1则为1,否则为0
 static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
  • exclusiveCount(int c) 静态方法,获得持有写状态的锁的次数。(顾名思义,写锁是独占锁,重入次数)

  • sharedCount(int c) 静态方法,获得持有读状态的锁的线程数量。不同于写锁,读锁可以同时被多个线程持有。而每个线程持有的读锁支持重入的特性,所以需要对每个线程持有的读锁的数量单独计数,这就需要用到 HoldCounter 计数器。(HoldCounter是Sync的静态内部类,主要起计算每个线程的数量的作用。)

FROM 《Java并发编程的艺术》的 「5.4 读写锁」 章节

如果当前同步状态state不为0,那么先计算低16位写状态,如果低16为为0,也就是写 状态为0则表示高16为不为0,也就是读状态不为0,则读获取到锁;如果此时低16为 不为0则抹去高16位得出低16位的值,判断是否与state值相同,如果相同则表示写获 取到锁。同样如果state不为0,低16为不为0,且低16位值不等于state,也可以通过 state的值减去低16位的值计算出高16位的值。上述计算过程都是通过位运算计算出来 的。

上图中为什么表示当前状态有一个线程已经获取了写锁,且重入了两次,同时也获取了两次读锁。这是因为:

1、写锁的获取与释放

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

2、读锁的获取与释放

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

3、锁降级

看到上述这两段似乎还是找不到为什么会出现高位和低位都不为0的情况怎样确定当前 线程获取的是写锁的解答,这就需要我们从另一个需要注意的地方说起:锁降级。

何为锁降级,意思主要是为了保证数据的可见性,假如有一个线程A已经获取了写锁, 并且修改了数据,如果当前线程A不获取读锁而直接释放写锁,此时,另一个线程B获 取到了写锁并修改了数据,那么当前线程A无法感知线程B的数据更新。如果当前线程 A获取读锁,即遵循降级的步骤,则线程B将会被阻塞,直到当前线程A使用数据并释 放读锁之后,线程B才能获取写锁进行数据更新。 另外,锁降级中读锁的获取是必要的!!!

正是由于锁降级的存在,才会出现上图中高16位和低16为都不为0,但可以确定是写 锁的问题。可以得出结论,如果高16位或者低16位为0,那么我们就可以判断获取到的是写锁或读锁;如果高16位和低16位都不为0那获取到的应该是写锁。就是说如果 当前线程已经获取到写锁的话,该线程也是可以通过CAS线程安全的增加读状态的,成功获取读锁。 虽然,为了保证数据的可见性引入锁降级可以将写锁降级为读锁,但是却不可以锁升级,将读锁升级为写锁的,也就是不会出现:当前线程已经获取到读锁了,通过某种方式增加写状态获取到写锁的情况。不允许升级的原因也是保证数据的可见性,如果读锁已被多个线程获取,其中任意线程成功获取了写锁并更新了数据,则其更新对其他获取到读锁的线程是不可见的。

3.1 构造方法

在上面的构造方法中,我们已经看到基于 fair 参数,创建 FairSync 还是 NonfairSync 对象。

3.2 getThreadId

getThreadId(Thread thread) 静态方法,获得线程编号。代码如下:

/**
  * 返回线程对应的线程id,不能直接通过Thread.getId(),因为这个方法不是final修饰,不知道有没有被重写 
  */
 static final long getThreadId(Thread thread) {
     return UNSAFE.getLongVolatile(thread, TID_OFFSET);
 }
 ​
 // Unsafe mechanics
 private static final sun.misc.Unsafe UNSAFE;
 private static final long TID_OFFSET;
 static {
     try {
         UNSAFE = sun.misc.Unsafe.getUnsafe();
         Class<?> tk = Thread.class;
         TID_OFFSET = UNSAFE.objectFieldOffset
             (tk.getDeclaredField("tid"));
     } catch (Exception e) {
         throw new Error(e);
     }
 }
  • 按道理说,直接调用线程对应的 Thread的getId() 方法即可,代码如下:

    private long tid;
     ​
     public long getId() {
         return tid;
     }
    • 但是实际上,Thread 的这个方法是非 final 修饰的,也就是说,如果我们有实现 Thread 的子类,完全可以重写这个方法,所以可能导致无法获得 tid 属性。因此上面的方法,使用 Unsafe*直接获得 tid 属性。

4. 读锁和写锁

上面提到,ReentrantReadWriteLock 的读锁和写锁,基于它内部的 Sync 实现,所以具体的实现方法,就是对内部的 Sync 的方法的调用

4.1 ReadLock

ReadLock 是 ReentrantReadWriteLock 的内部静态类,实现 java.util.concurrent.locks.Lock 接口,读锁实现类。

4.1.1 构造方法 

private final Sync sync;
 ​
 protected ReadLock(ReentrantReadWriteLock lock) {
     sync = lock.sync;
 }
  • sync 字段,通过 ReentrantReadWriteLock 的构造方法,传入并使用它的 Sync 对象。

4.1.2 lock

@Override
 public void lock() {
     sync.acquireShared(1);
 }
  • 调用 AQS 的 acquireShared(int arg) 方法,共享式获得同步状态。所以,读锁可以同时被多个线程获取。

4.1.3 lockInterruptibly

@Override
 public void lockInterruptibly() throws InterruptedException {
     sync.acquireSharedInterruptibly(1);
 }

4.1.4 tryLock

@Override
public boolean tryLock() {
    return sync.tryReadLock();
}
  • 和ReentrantLock的tryLock一样。

  • tryLock() 实现方法,在实现时,希望能快速的获得是否能够获得到锁,因此即使在设置为 fair = true ( 使用公平锁 ),依然调用 Sync#tryReadLock() 方法。

  • 如果真的希望 tryLock() 还是按照是否公平锁的方式来,可以调用 #tryLock(0, TimeUnit) 方法来实现。

4.1.5 tryLock

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

4.1.6 unlock

@Override
public void unlock() {
    sync.releaseShared(1);
}
  • 调用 AQS 的 releaseShared(int arg) 方法,共享式释放同步状态。

4.1.7 newCondition

@Override
public Condition newCondition() {
    throw new UnsupportedOperationException();
}
  • 不支持 Condition 条件。

4.2 WriteLock

WriteLock 的代码,类似 ReadLock 的代码,差别在于独占式获取同步状态。

WriteLock 是 ReentrantReadWriteLock 的内部静态类,实现 java.util.concurrent.locks.Lock 接口,写锁实现类。

4.2.1 构造方法

private final Sync sync;

protected ReadLock(ReentrantReadWriteLock lock) {
    sync = lock.sync;
}
  • sync 字段,通过 ReentrantReadWriteLock 的构造方法,传入并使用它的 Sync 对象。

4.2.2 lock

@Override
public void lock() {
    sync.acquire(1);
}
  • 调用 AQS 的 #.acquire(int arg) 方法,独占式获得同步状态。所以,写锁只能同时一个线程获取。

4.2.3 lockInterruptibly

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

4.2.4 tryLock

@Override
 public boolean tryLock( ) {
    return sync.tryWriteLock();
}
  • 同上面的

4.2.5 tryLock

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

4.2.6 unlock

@Override
public void unlock() {
    sync.release(1);
}
  • 调用 AQS 的 release(int arg) 方法,独占式释放同步状态。

4.2.7 newCondition

@Override
public Condition newCondition() {
    return sync.newCondition();
}
  • 调用 Sync的newCondition() 方法,创建 Condition 对象。

4.2.8 isHeldByCurrentThread

public boolean isHeldByCurrentThread() {
    return sync.isHeldExclusively();
}
  • 调用 Sync的isHeldExclusively() 方法,判断是否被当前线程独占锁。

4.2.9 getHoldCount

public int getHoldCount() {
    return sync.getWriteHoldCount();
}
  • 调用 Sync的getWriteHoldCount() 方法,返回当前线程独占锁的持有数量。

5. Sync 抽象类

Sync 是 ReentrantReadWriteLock 的内部静态类,实现 AbstractQueuedSynchronizer 抽象类,同步器抽象类。它使用 AQS 的 state 字段,来表示当前锁的持有数量,从而实现可重入读写锁的特性。

5.1 构造方法

// transient保证不能被序列化
private transient ThreadLocalHoldCounter readHolds; // 当前线程的读锁持有数量
private transient Thread firstReader = null; // 第一个获取读锁的线程
private transient int firstReaderHoldCount; // 第一个获取读锁的重入数

private transient HoldCounter cachedHoldCounter; // 最后一个获得读锁的线程的 HoldCounter 的缓存对象

Sync() {
    readHolds = new ThreadLocalHoldCounter();
    setState(getState()); // ensures visibility of readHolds
}
  • 见下面的HoldCounter

5.2 writerShouldBlock

abstract boolean writerShouldBlock();
  • 获取写锁时,如果有前序节点也获得锁时,是否阻塞。NonefairSync 和 FairSync 下有不同的实现。详细解析,见 下面的Sync 实现类。

5.3 readerShouldBlock

abstract boolean readerShouldBlock();
  • 获取读锁时,如果有前序节点也获得锁时,是否阻塞。NonefairSync 和 FairSync 下有不同的实现。详细解析,见下面的Sync 实现类 。

5.4 【写锁】tryAcquire

@Override
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;
}
  • 该方法和 ReentrantLock 的 tryAcquire(int arg) 大致一样,差别在判断重入时,增加了一项条件:读锁是否存在。因为要确保写锁的操作对读锁是可见的。如果在存在读锁的情况下允许获取写锁,那么那些已经获取读锁的其他线程可能就无法感知当前写线程的操作。因此只有等读锁完全释放后,写锁才能够被当前线程所获取,一旦写锁获取了,所有其他读、写线程均会被阻塞。

  • 调用 writerShouldBlock() 抽象方法,若返回 true ,则获取写锁失败。

5.5 【读锁】tryAcquireShared

tryAcqurireShared(int arg) 方法,尝试获取读同步状态,获取成功返回 >= 0 的结果,否则返回 < 0 的结果。代码如下:

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)) { //修改高16位的状态,所以要加上2^16
        /*
         * 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);
}
  • 读锁获取的过程相对于独占锁而言会稍微复杂下,整个过程如下:

    1. 因为存在锁降级情况,如果存在写锁且锁的持有者不是当前线程,则直接返回失败,否则继续。

    2. 依据公平性原则,调用 readerShouldBlock() 方法来判断读锁是否不需要阻塞,读锁持有线程数小于最大值(65535),且 CAS 设置锁状态成功,执行以下代码,并返回 1 。如果不满足任一条件,则调用 fullTryAcquireShared(Thread thread) 方法,详细解析,见下面。

5.5.1 fullTryAcquireShared

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) // 计数为 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)) { //修改高16位的状态,所以要加上2^16  
           //如果是第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;
       }
   }
}
  • 该方法会根据“是否需要阻塞等待”,“读取锁的共享计数是否超过限制”等等进行处理。如果不需要阻塞等待,并且锁的共享计数没有超过限制,则通过 CAS 尝试获取锁,并返回 1 。所以,fullTryAcquireShared(Thread) 方法,是 tryAcquireShared(int unused) 方法的自旋重试的逻辑。

5.6 【写锁】tryRelease

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;
}
  • 写锁释放锁的整个过程,和独占锁 ReentrantLock 相似,每次释放均是减少写状态,当写状态为 0 时,表示写锁已经完全释放了,从而让等待的其他线程可以继续访问读、写锁,获取同步状态。同时,此次写线程的修改对后续的线程可见。

5.7 【读锁】tryReleaseShared

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;
    }
}
  • unmatchedUnlockException() 方法,返回 IllegalMonitorStateException 异常。代码如下:

    private IllegalMonitorStateException unmatchedUnlockException() {
        return new IllegalMonitorStateException(
            "attempt to unlock read lock, not locked by current thread");
    }
    • 出现的情况是,unlock 读锁的线程,非获得读锁的线程。正常使用的情况,不会出现该情况。

5.8 tryWriteLock

tryWriteLock() 方法,尝试获取写锁。

  • 若获取成功,返回 true 。

  • 若失败,返回 false 即可,不进行等待排队

代码如下:

final boolean tryWriteLock(){
    Thread current = Thread.currentThread();
    int c = getState();
    if(c != 0){
        int w = exclusiveCount(c); // 获得现在写锁获取的数量
        if(w == 0 || current != getExclusiveOwnerThread()){  // 判断是否是其他的线程获取了写锁或者当前没有线程占据写锁。若是,返回 false
            return false;
        }
        if(w == MAX_COUNT){ // 超过写锁上限,抛出 Error 错误
            throw new Error("Maximum lock count exceeded");
        }
    }

    if(!compareAndSetState(c, c + 1)){ //  CAS 设置同步状态,尝试获取写锁。若失败,返回 false
        return false;
    }
    setExclusiveOwnerThread(current); // 设置持有写锁为当前线程
    return true;
}

5.9 tryReadLock

tryReadLock() 方法,尝试获取读锁。

  • 若获取成功,返回 true 。

  • 若失败,返回 false 即可,不进行等待排队

代码如下:

 
final boolean tryReadLock() {
    Thread current = Thread.currentThread();
    for (;;) {
        int c = getState();
       //exclusiveCount(c)计算写锁
        //如果存在写锁,且锁的持有者不是当前线程,直接返回-1
        //存在锁降级问题,后续阐述
        if (exclusiveCount(c) != 0 &&
            getExclusiveOwnerThread() != current)
            return false;
        // 读锁
        int r = sharedCount(c);
        if (r == MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        if (compareAndSetState(c, c + SHARED_UNIT)) {
            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 true;
        }
    }
}

5.10 isHeldExclusively

protected final boolean isHeldExclusively() {
    // While we must in general read state before owner,
    // we don't need to do so to check if current thread is owner
    return getExclusiveOwnerThread() == Thread.currentThread();
}

5.11 newCondition

final ConditionObject newCondition() {
    return new ConditionObject();
}

6. Sync 实现类

6.1 NonfairSync

NonfairSync 是 ReentrantReadWriteLock 的内部静态类,实现 Sync 抽象类,非公平锁实现类。代码如下:

static final class NonfairSync extends Sync {

    @Override
    final boolean writerShouldBlock() {
        return false; // writers can always barge
    }
    
    @Override
    final boolean readerShouldBlock() {
        /* As a heuristic to avoid indefinite writer starvation,
         * block if the thread that momentarily appears to be head
         * of queue, if one exists, is a waiting writer.  This is
         * only a probabilistic effect since a new reader will not
         * block if there is a waiting writer behind other enabled
         * readers that have not yet drained from the queue.
         */
        return apparentlyFirstQueuedIsExclusive();
    }
    
}
  • 因为写锁是独占排它锁,所以在非公平锁的情况下,需要调用 AQS 的 #apparentlyFirstQueuedIsExclusive() 方法,判断是否当前写锁已经被获取。代码如下:

    final boolean apparentlyFirstQueuedIsExclusive() {
        Node h, s;
        return (h = head) != null &&
            (s = h.next)  != null &&
            !s.isShared()         && // 非共享,即独占
            s.thread != null;
    }

     

6.2 FairSync

FairSync 是 ReentrantReadWriteLock 的内部静态类,实现 Sync 抽象类,公平锁实现类。代码如下:

static final class FairSync extends Sync {

    @Override
    final boolean writerShouldBlock() {
        return hasQueuedPredecessors();
    }
    
    @Override
    final boolean readerShouldBlock() {
        return hasQueuedPredecessors();
    }
    
}
  • 调用 AQS 的 #hasQueuedPredecessors() 方法,是否有前序节点,即自己不是首个等待获取同步状态的节点。

7. HoldCounter

在读锁获取锁和释放锁的过程中,我们一直都可以看到一个变量 rh (HoldCounter ),该变量在读锁中扮演着非常重要的作用。

我们了解读锁的内在机制其实就是一个共享锁,为了更好理解 HoldCounter ,我们暂且认为它不是一个锁的概率,而相当于一个计数器一次共享锁的操作就相当于在该计数器的操作。获取共享锁,则该计数器 + 1,释放共享锁,该计数器 - 1。只有当线程获取共享锁后才能对共享锁进行释放、重入操作。所以HoldCounter 的作用就是当前线程持有共享锁的数量,这个数量必须要与线程绑定在一起,否则操作其他线程锁就会抛出异常。

HoldCounter 是 Sync 的内部静态类。

 
static final class HoldCounter {
    int count = 0; // 计数器
    final long tid = getThreadId(Thread.currentThread()); // 线程编号
}
  • ThreadLocalHoldCounter 是 Sync 的内部静态类。

    static final class ThreadLocalHoldCounter extends ThreadLocal<HoldCounter> {
        
        @Override
        public HoldCounter initialValue() {
            return new HoldCounter();
        }
    }

     

通过 ThreadLocalHoldCounter 类,HoldCounter 就可以与线程进行绑定了。故而,HoldCounter 应该就是绑定线程上的一个计数器,而 ThreadLocalHoldCounter 则是线程绑定的 ThreadLocal。从上面我们可以看到 ThreadLocal 将 HoldCounter 绑定到当前线程上,同时 HoldCounter 也持有线程编号,这样在释放锁的时候才能知道 ReadWriteLock 里面缓存的上一个读取线程(cachedHoldCounter)是否是当前线程。这样做的好处是可以减少ThreadLocal.get() 方法的次调用数,因为这也是一个耗时操作。需要说明的是,HoldCounter 绑定线程编号而不绑定线程对象的原因是,避免 HoldCounter 和 ThreadLocal 互相绑定而导致 GC 难以释放它们(尽管 GC 能够智能的发现这种引用而回收它们,但是这需要一定的代价),所以其实这样做只是为了帮助 GC 快速回收对象而已。

看到这里我们明白了 HoldCounter 作用了,我们在看一个获取读锁的代码段:

//如果获取读锁的线程为第一次获取读锁的线程,则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
}
  • 这里解释下为何要引入 firstReaderfirstReaderHoldCount 变量。这是为了一个效率问题,firstReader是不会放入到 readHolds 中的,如果读锁仅有一个的情况下,就会避免查找 readHolds

8. 锁降级

锁降级就意味着写锁是可以降级为读锁的,但是需要遵循先获取写锁、获取读锁在释放写锁的次序。

注意:如果当前线程先获取写锁,然后释放写锁,再获取读锁这个过程不能称之为锁降级,锁降级一定要遵循那个次序。

在获取读锁的方法 tryAcquireShared(int unused) 中,有一段代码就是来判读锁降级的:

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

锁降级中读锁的获取释放为必要?肯定是必要的。试想,假如当前线程 A 不获取读锁而是直接释放了写锁,这个时候另外一个线程 B 获取了写锁,那么这个线程 B 对数据的修改是不会对当前线程 A 可见的。如果获取了读锁,则线程B在获取写锁过程中判断如果有读锁还没有释放则会被阻塞,只有当前线程 A 释放读锁后,线程 B 才会获取写锁成功。

使用实例:

private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private Lock readLock = lock.readLock();
private Lock writeLock = lock.writeLock();
private boolean update;
public void processData() {
    readLock.lock(); //读锁获取
    if (!update) {
        readLock.unlock(); //必须先释放读锁
        writeLock.lock(); //锁降级从获取写锁开始
        try {
            if (!update) {
                //准备数据流程(略)
                update = true;
            }
            //获取读锁。在写锁持有期间获取读锁
            //此处获取读锁,是为了防止,当释放写锁后,又有一个线程T获取锁,对数据进行改变,
            //而当前线程下面对改变的数据无法感知。
            //如果获取了读锁,则线程T则被阻塞,直到当前线程释放了读锁,那个T线程才有可能获取写锁。
            readLock.lock();
        } finally {
            writeLock.unlock();//释放写锁
        }
        //锁降级完成
    }

    try {
        //使用数据的流程
    } finally {
        readLock.unlock(); //释放读锁
    }
}

9.总结

1、读锁的重入是允许多个申请读操作的线程的,而写锁同时只允许单个线程占有,该 线程的写操作可以重入。

2、如果一个线程占有了写锁,在不释放写锁的情况下,它还能占有读锁,即写锁降级为读锁。

3、对于同时占有读锁和写锁的线程,如果完全释放了写锁,那么它就完全转换成了读锁,以后的写操作无法重入,在写锁未完全释放时写操作是可以重入的。

4、公平模式下无论读锁还是写锁的申请都必须按照AQS锁等待队列先进先出的顺序。非公平模式下读操作插队的条件是锁等待队列head节点后的下一个节点是 SHARED型节点,写锁则无条件插队。

5、读锁不允许newConditon获取Condition接口,而写锁的newCondition接口实现方 法同ReentrantLock。

 

参照:《【死磕 Java 并发】—– J.U.C 之读写锁:ReentrantReadWriteLock》

参照:https://xuliugen.blog.csdn.net/article/details/78375986

posted @ 2019-03-21 17:26  白晨冬阳  阅读(83)  评论(0编辑  收藏  举报