ReentrantReadWriteLock-学习

1,897 阅读8分钟

ReentrantReadwriteLock,简称RrwLock;

简介

RrwLock实现了ReadWriteLock接口,ReadLock,WriteLock实现了Lock接口规范;

ReadLock共享锁,实现了AQS的tryAcquireShare、tryReleaseShare;

WriteLock独占锁,实现了AQS的tryAcquire、tryRelease;

写操作:与任何读锁互斥,与除自己之外(可重入)的写锁互斥;

读操作:与任何读锁都不互斥,与除自己之外的写锁互斥;

基本组成-类

  • Sync

    • HoldCounter:每个线程获取到读锁的计数器
    • ThreadLocalHoldCounter:继承ThreadLocal;重写ThreadLocal#initialValue方法,返回HoldCounter
  • FairSync

  • NonfairSync

  • ReadLock

  • WriteLock

Sync 基本属性

  • Thread firstReader

记录第一个获取到读锁,且还未释放读锁的线程

  • in t firstReaderHoldCount

第一个读锁线程的重入数

  • HoldCounter cachedHoldCounter

指向某个持有读锁的线程的HoldCounter对象

  • ThreadLocalHoldCounter readHolds

记录每个线程分别持有多少读锁

方法

exclusiveCount

static final int SHARED_SHIFT   = 16;
​
// 10000000000000000 - 1 = 01111111111111111   ===> 16个1
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; 
​
/** 
  * 返回共享状态下锁的数量 
  * c >>> SHARED_SHIFT :
  * **************** **************** 将后16位*移走,保留c的前16位*
  */
static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
​
/** 
  * 返回独占状态下锁的数量
  * 
  * 1与任何数进行&操作,都等于任何数本身,0与任何数&操作都等于0
  * 参数c为32位* :    **************** ****************
  * EXCLUSIVE_MASK: 0000000000000000 1111111111111111
  * 所以c & EXCLUSIVE_MASK = c的后16位
  */
 static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

tryAcquire 写锁,独占式获取资源

/**
 * Sync#tryAcquire
 * 资源已被获取(state != 0):
 *    读锁存在,返回false
 *    写锁存在但不是当前线程,返回false。写锁可重入
 *    超过count最大值(65535),返回false
 * 资源未被获取:
 *    写阻塞,返回false
 *    CAS设置状态失败,返回false
 */
protected final boolean tryAcquire(int acquires) {
  
    Thread current = Thread.currentThread();
    int c = getState();
    // 获取写锁的状态 
    int w = exclusiveCount(c);
    // 资源已被获取
    if (c != 0) {
        // 资源被获取并且写锁数量是0,认为是有 读锁 存在
        // 读锁存在(读写互斥) 或 当前线程不占有锁,资源获取失败
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        // 是否超过最大count限制
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        // 更新状态
        setState(c + acquires);
        return true;
    }
    // 如果需要写阻塞 或 CAS设置状态失败,资源获取失败
    if (writerShouldBlock() ||
        !compareAndSetState(c, c + acquires))
        return false;
    // 标记当前线程占有资源
    setExclusiveOwnerThread(current);
    return true;
}

writerShouldBlock:Sync的抽象方法,FairSync、NonfairSync负责实现

/**
 * FairSync#writerShouldBlock
 */
final boolean writerShouldBlock() {
    // 调用AQS,查看是否有前一个节点,有的话返回ture,进行阻塞,这样符合公平锁
    return hasQueuedPredecessors();
}
​
/**
 * NonfairSync#writerShouldBlock
 */
final boolean writerShouldBlock() {
    // 永远不会阻塞
    return false;
}

tryRelease 写锁释放资源

/**
 * Sync#tryRelease
 * 写锁数量为0代表可以释放资源
 */
protected final boolean tryRelease(int releases) {
    // 获取到资源的线程是否与当前线程一致
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    int nextc = getState() - releases;
    // 写锁数量为0代表可以释放资源,free为true
    boolean free = exclusiveCount(nextc) == 0;
    if (free)
        setExclusiveOwnerThread(null);
    setState(nextc);
    return free;
}

tryAcquireShared 读锁,共享式获取资源,

static final int SHARED_SHIFT   = 16;
// 10000000000000000 = 65536 
static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
​
​
/*
 * Sync#tryAcquireShared,快速获取资源
 *
 * 满足以下情况可以获取到资源:
 *   1. 没有写锁存在 或 本线程的写锁存在 
 *   2. 不需要读阻塞 且 读锁数量未超过最大值 且 CAS 设置状态成功
 * 
 */
protected final int tryAcquireShared(int unused) {
 
    Thread current = Thread.currentThread();
    int c = getState();
    // 如果有非本线程的写锁存在,返回false (锁降级)
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;
    // 获取全部线程的读锁数量
    int r = sharedCount(c);
    // 不需要读阻塞 且 读锁数量<最大值(65535) 且 CAS设置状态成功,满足这3个条件代表获取锁成功
    if (!readerShouldBlock() &&
        r < MAX_COUNT &&
        // CAS设置状态会从65536开始加,因为读锁状态是state前16
        compareAndSetState(c, c + SHARED_UNIT)) {
        // 读锁数量为0,设置当前线程作为第一个读锁,并初始第一个读锁的count数量 1
        if (r == 0) {
            firstReader = current;
            firstReaderHoldCount = 1;
        } else if (firstReader == current) {
            // 第一个读锁线程重入,数量+1
            firstReaderHoldCount++;
        } else {
            // 当前线程为第一个读锁线程之外的线程,获取当前线程的HoldCounter   
            HoldCounter rh = cachedHoldCounter;
           // 如果cachedHoldCounter为空或cachedHoldCounter不是当前线程的,
           // 则通过ThreadLocal获取一个新的HoldCounter,并将cachedHoldCounter设置为当前线程
            if (rh == null || rh.tid != getThreadId(current))
                cachedHoldCounter = rh = readHolds.get();
            else if (rh.count == 0)
                readHolds.set(rh);
            // 当前线程HoldCounter的count++
            rh.count++;
        }
        return 1;
    }
    // 获取锁失败,自旋重新获取
    return fullTryAcquireShared(current);
}
​
-------------------------------------------------------------------------
​
假设有3个线程获取读锁,那么state、读状态、释放锁后的state变化如下:
​
第一个线程获取到读锁:
  state:65536 => 1 0000 0000 0000 0000
  读状态:1 
  释放锁后的state: 65536 - 65536 = 0
​
第二个线程获取到读锁:
  state:131072 => 10 0000 0000 0000 0000
  读状态:2
  释放锁后的state:131072 - 65536 = 65536 
​
第三个线程获取到读锁:
  state:196608 => 11 0000 0000 0000 0000  
  读状态:3 
  释放锁后的state:196608 - 65536 = 131072

readerShouldBlock Sync的抽象方法,FairSync、NonfairSync负责实现

/**
 * FairSync#readerShouldBlock
 */
final boolean readerShouldBlock() {
    // 调用AQS,查看是否有前一个节点,有的话返回ture,进行阻塞,这样符合公平锁
    return hasQueuedPredecessors();
}
​
/**
 * NonfairSync#readerShouldBlock 
 * 
 * 1. AQS等待队列的第一个节点为写锁,则阻塞当前读锁(只针对新线程),返回false,尽量优先写锁,避免读多写少时,写锁长时间等待;
 * 2. 但也不能完全避免写锁长时间等待,如果AQS等待队列的第一个节点为读锁,第二个节点为写锁,那么当前读锁会获取成功,
 * 如果读多写少,也会导致第二个写锁一直阻塞。 
 * 3. 为了防止写锁长时间等待,非公平锁并不是完全的非公平
 */
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.
     */
   // 调用AQS,判断head节点的下一个节点 是否为独占模式(这里代表写锁),是的话返回true
    return apparentlyFirstQueuedIsExclusive();
}

fullTryAcquireShared

/**
 *  Sync#fullTryAcquireShared
 * tryAcquireShared的完整版,处理tryAcquireShare中CAS失败,或需要读阻塞的情况
 *
 * 返回-1,结束自旋的情况:
 *   1. 有非本线程的写锁存在
 *   2. 需要读阻塞,但当前线程不可重入(HoldCounter.count == 0)
 */
final int fullTryAcquireShared(Thread current) {
    
    HoldCounter rh = null;
    for (;;) {
      
     // 第一部分:筛选不符合条件的线程,返回-1,结束自旋
      
        int c = getState();
      // 判断是否有非本线程的写锁存在,如果有返回-1
        if (exclusiveCount(c) != 0) {
            if (getExclusiveOwnerThread() != current)
                return -1;
          
        // 判断是否需要读阻塞
        // 就算需要读阻塞,但如果线程已经获取到了读锁,也是可以继续重入,不需要返回-1,再次排队  
        } else if (readerShouldBlock()) {
            // 如果当前线程是firstReader且不为null,说明已经获取到了读锁,可继续重入
            if (firstReader == current) {
                
            } else { 
               // 当前线程不是firstReader,则获取线程对应的HoldCounter:通过缓存或ThreadLocal
                if (rh == null) {
                    rh = cachedHoldCounter;
                    // 如果缓存的不是当前线程的HoldCounter,则从ThreadLocal获取
                    if (rh == null || rh.tid != getThreadId(current)) {
                        rh = readHolds.get();
                       // rh.count == 0说明当前线程第一次获得读锁,在读阻塞下,要remove
                       // 否则说明rh.count > 0,已获取到读锁,可重入
                        if (rh.count == 0)
                            readHolds.remove();
                    }
                }
                // 当前线程第一次获得读锁,返回-1
                if (rh.count == 0)
                    return -1;
            }
        }
      
      
      // 第二部分:CAS设置资源状态,失败后进行循环
      
      // 是否超过读锁最大限制
        if (sharedCount(c) == MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
      // CAS设置state
        if (compareAndSetState(c, c + SHARED_UNIT)) {
          
            // 设置第一个获取到读锁的线程,读锁数量
            if (sharedCount(c) == 0) {
                firstReader = current;
                firstReaderHoldCount = 1;
            // 如果是当前线程是第一个获取读锁线程,读锁数量++  
            } else if (firstReader == current) {
                firstReaderHoldCount++;
            } else {
                // 获取缓存的cacheHold
                if (rh == null)
                    rh = cachedHoldCounter;
              // 如果当前缓存的cacheHold不是当前线程的,初始化一个新的HoldCounter
                if (rh == null || rh.tid != getThreadId(current))
                    rh = readHolds.get();
                else if (rh.count == 0)
                    readHolds.set(rh);
               
              // 将当前线程的读锁count +1 
                rh.count++;
                cachedHoldCounter = rh; // cache for release
            }
            return 1;
        }
    }
}

tryReleaseShare 读锁释放资源

/*
 * Sync#tryReleaseShared
 * 读锁跟写锁都为0,才认为释放完成
 * 
 */
protected final boolean tryReleaseShared(int unused) {
    Thread current = Thread.currentThread();
    // 当前线程是第一个获取到读锁的线程
    if (firstReader == current) {
        // ==1,代表没被重入,并设为null,代表第一个被占用的线程已被释放
        if (firstReaderHoldCount == 1)
            firstReader = null;
        else // 代表被重入,count--
            firstReaderHoldCount--;
    } else {
      
       // 从缓存跟ThreadLocal中获取其它线程的HoldCounter
        HoldCounter rh = cachedHoldCounter;
        if (rh == null || rh.tid != getThreadId(current))
            rh = readHolds.get();
        int count = rh.count;
        // 代表没被重入,可以被释放了,从ThreadLocal中删除
        if (count <= 1) {
            readHolds.remove();
            if (count <= 0)
                throw unmatchedUnlockException();
        }
       // 代表被重入,--count
        --rh.count;
    }
    for (;;) {
        int c = getState();
        int nextc = c - SHARED_UNIT;
        if (compareAndSetState(c, nextc))
            // 读锁跟写锁都为0,才认为释放完成
            return nextc == 0;
    }
}
​

知识点

HoldCounter中使用线程id,而不是线程引用,是为了防止影响垃圾回收?

ThreadLocal中key为Thread,而HoldCounter再引用Thread,会造成互相绑定而GC难以释放它们(尽管GC能够智能的发现这种引用而回收它们,但是这需要一定的代价【待研究】),所以其实这样做只是为了帮助GC快速回收对象而已。

ReadLock、WriteLock、或者是ReentrantLock 为什么不继承AQS,而是要Sync去继承能?

让Sync单独继承AQS, 可以让每个类的指责更明显,类更好维护

为什么某些字段要用 transient 修饰?

  • private transient ThreadLocalHoldCounter readHolds;
  • private transient HoldCounter cachedHoldCounter;
  • private transient Thread firstReader;
  • private transient int firstReaderHoldCount;

从被修饰的字段可以看出,都是记录线程相关的信息(重入数、线程id), transient是防止被序列化,线程这种执行完就没了,如果持久化后再反序列化返回会造成不必要的麻烦,比如:无法释放锁。

RrwLock什么时候会锁降级?

获取读锁时,如果有当前线程的写锁存在,也可以继续获取读锁,当写锁释放时,写锁会变为读锁,这就是锁降级;

如果当前线程的读写锁同时存在,需要写锁释放后,读锁才会成功释放;

锁降级Demo分析:

public static void main(String[] args) {
       ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
        Lock writeLock = reentrantReadWriteLock.writeLock();
        Lock readLock = reentrantReadWriteLock.readLock();
        writeLock.lock();
        readLock.lock();
        writeLock.unlock();
        readLock.unlock();
}
​
​
获取写锁时,state:1,写锁状态:1,读锁状态:0
​
获取读锁时,state:65537,写锁状态:1,读锁状态:1
​
释放写锁时,state:65536,写锁状态:0,读锁状态:1 ,这时候变成读锁了,锁降级
​
释放读锁时,state:65536,写锁状态:0,读锁状态:0

NonfairSync#readerShouldBlock中, 为什么AQS等待队列的首节点为写锁,则阻塞当前读锁?

  1. 避免非公平读锁下的写饥饿问题(写锁一直获取不到资源),公平锁不会存在问题,等待队列是FIFO执行,总会被唤醒;
  1. 读锁是共享锁,只要不超过最大限制65535,就可以一直获取, 如果读多写少的话,可能导致等待队列首节点写锁一直无法被唤醒;
  1. 如果首节点是读锁a,读锁a下一个是写锁a,这样仍无法避免写锁a的写饥饿问题。

StampedLock 替代 RrwLock?

【待研究】

RrwLock跟ReentrantLock相比有哪些优势?

RrwLock 将读、写拆分,增加了读操作的效率,不需要将读,写串行执行

为什么读锁需要记录每个线程的读锁count,其它并发锁不需要能?

ReentrantLock:独占模式,AQS.state可以表示当前线程重入数;

Semphore:共享模式,不关注每个线程重入次数,只需关注还剩多少资源,可以被获取;

CountDownLatch: 共享模式,不关注每个线程重入次数,只需关注还剩多少资源,没被释放;

RrwLock:读锁-共享模式,在#fullTryAcquireShare中,如果是读阻塞,需要根据线程重入次数(HoldCounter.count),判断当前线程是新线程还是老线程,读阻塞时新线程不允许获取。

为什么RrwLock中第一个线程不放到ThreadLocal中,而要定义firstThread?

为了提高性能,减少从ThreadLocal中读取的次数,包括cacheHoldCounter也是为了提高性能。

之前1.5的时候没有firstThread,1.6之后有人提了个bug,ThreadLocal内存溢出了,ThreadLocal没有remove,后来解决bug的时候,优化了这个。 参考:blog.csdn.net/weixin_3658…