当前位置:   article > 正文

ReentrantReadWriteLock读写锁

reentrantreadwritelock

目录

一、前言

1、读写锁

2、可重入锁:

3、公平锁和非公平锁

二、接口

三、实现分析

3.1 读写状态的设计

3.1.1读位运算

3.1.2写位运算

3.2 写锁的获取与释放

3.2.1 tryAcquire方法

3.2.2 tryRelease方法

3.3 读锁的获取与释放

3.3.1 tryAcquireShared方法

3.3.2 tryReleaseShared

4、公平锁和非公平锁

4.1 使用原理

4.2 公平锁

4.3 非公平锁


一、前言

ReentrantReadWriteLock中包含三种锁:读写锁、可重入锁、公平/非公平锁

1、读写锁

2、可重入锁:

        又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。说的有点抽象,下面会有一个代码的示例。

  • 对于Java ReetrantLock而言,从名字就可以看出是一个重入锁,其名字是Re entrant Lock 重新进入锁。
  • 对于Synchronized而言,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁。
  1. synchronized void setA() throws Exception{
  2.   Thread.sleep(1000);
  3.   setB();
  4. }
  5. synchronized void setB() throws Exception{
  6.   Thread.sleep(1000);
  7. }

3、公平锁和非公平锁

  • 公平锁是指多个线程按照申请锁的顺序来获取锁。
  • 非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。

        对于Java ReetrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。 

 对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。

二、接口

        ReadWriteLock仅定义了获取读锁和写锁的两个方法,即ReadLock()方法和writeLock()方法,而其实现:ReentranReadWriteLock,除接口方法之外,还提供了一些便于外界监控其内部工作状态的方法:

特性 说明
公平性选择支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平
重进入该锁支持重进入。以读写线程为例:读线程在获取了读锁之后,能够再次获取读锁;而写线程在获取了写锁之后能够再次获取写锁,同时也可以获取读锁。
锁降级遵循获取写锁、获取读锁再获取写锁的次序,写锁可以降级成为读锁

三、实现分析

        分析ReentranReadWriteLock的实现,主要包括读写状态的设计、写锁的获取与释放、读锁的获取与释放以及锁降级。

3.1 读写状态的设计

        ReentranReadWriteLock中使用一个int state变量来维护读锁和写锁的状态。int类型为4字节32位,用state的高16位来存储读锁的状态,用低16位来存储写锁的状态。

3.1.1读位运算

位运算代码

  1. //偏移位数
  2. static final int SHARED_SHIFT = 16;
  3. //读锁计数基本单位
  4. static final int SHARED_UNIT = (1 << SHARED_SHIFT);
  5. //读锁、写锁可重入最大数量
  6. static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
  7. //获取低16位的条件
  8. static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
  9. //获取读锁重入数
  10. static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
  11. //获取写锁重入数
  12. static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

        因为ReentranReadWriteLock中的int整型变量state要同时维护读锁、写锁两种状态,所以ReentranReadWriteLock的是通过高低位切割来实现。

 int占4个字节,一个字节8个比特位,一共2位,切割一下,高16位表示读,底16位表示写。

读位运算

  1. //偏移位数
  2. static final int SHARED_SHIFT = 16;
  3. //读锁计数基本单位
  4. static final int SHARED_UNIT = (1 << SHARED_SHIFT);

读锁使用高16位,每次获取读锁成功+1,所以读锁计数基本单位是1的高16位,即1左移16位(1 << 16)。

 1左移16位等于65536,每次获取读锁成功都会加上65536,通过以下代码将65536右移16位,实际上每次获取读锁还是只是会加1,就好比你早上去上班去公司,晚上下班回家,最终晚上你还是回家了,左移,右移也是这个道理,是一个过程,最终的状态没变。

  1. //偏移位数
  2. static final int SHARED_SHIFT = 16;
  3. //获取读锁重入数
  4. static int sharedCount(int c) { return c >>> SHARED_SHIFT; }

上面shareCount函数通过位运算是做无符号右移16位获取读锁的重入数,为什么可以获取到呢?

 1左移16位为65536,65536再右移16位为1。

3的二进制

0000 0000 0000 0000 0000 0000 0000 0011

3右移16位

0000 0000 0000 0011 0000 0000 0000 0000

        比如我们获取到了3次读锁,就是65536 * 3 = 196608,转换下公式就是3左移16位等于196608196608右移16位等于3

 

虽然我们每次获取到读锁都会+65536,但是获取读锁时会做右移16位,所以效果和+1是一样。

3.1.2写位运算

  1. //偏移位数
  2. static final int SHARED_SHIFT = 16;
  3. //获取低16位的条件
  4. static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
  5. //获取写锁重入数
  6. static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

剩下的写锁就非常简单,获取低16位不用左右移动,只要把高16位全部补0即可。

         反推一下,因为不需要左右移动,其实就和正常的数字一样,只不过因为高16位补0,导致数值范围在0~65535,也就是说写锁获取成功直接+1就好了。

         EXCLUSIVE_MASK变量,1右移16位后-1,得到6553565535的二进制就是111111111111111

现在来看exclusiveCount函数,该函数内做了位运算&&又称""运算。

""运算是两个二进制,每位数运算,运算规则如下

  1. 0&0=0
  2. 0&1=0
  3. 1&0=0
  4. 1&1=1

如果相对应位都是1,则结果为1,否则为0

可能有些读者大大还是不太明白,下面放张图16位二进制""运算图

         我们发现""运算时,只要有一方为0,那结果一定是0,所以为了切割低16位,可以使用&来完成。

 

        从上图可以看出,EXCLUSIVE_MASK16位都是0,低16位都是1,和它&的变量,高16位全部会变成0,低16位全部保留下来,最终达到获取低16位效果。

c & EXCLUSIVE_MASK,假设c1&的过程如下图

 这样看可能没太大感觉,我们把数值调大点,假设c6553665537&的过程如下图

现在有感觉了吧,c的高16位都会变成0,低16位会原样保留,最终达到获取低16位效果。

  EXCLUSIVE_MASK范围在0~65535,所以c的范围也不会超过0~65535,因为超过了也会通过& EXCLUSIVE_MASK回到0~65535

3.2 写锁的获取与释放

        ReentranReadWriteLock的写锁的获取与释放实现基于同步器AbstractQueuedSynchronizer,与其它独占锁的差异就在于对同步器中tryAcquire()和tryRelease()的重写上,所以下面仅讨论这两个方法。AbstractQueuedSynchronizer独占锁释放获取实现见:AbstractQueuedSynchronizer独占式同步状态获取与释放。

3.2.1 tryAcquire方法

官方给出的注释如下:

  1. 若读锁数或写锁数非空,并且当前线程不是读锁或写锁的持有者,获取失败。
  2. 若当前线程获取写锁的数量已经达到最大值,失败。
  3. 否则,当前线程获得锁。
  1. protected final boolean tryAcquire(int acquires) {
  2. /*
  3. * Walkthrough:
  4. * 1. 若读锁数或写锁数非空,并且当前线程不是读锁或写锁的持有者,获取失败。
  5. * 2. 若当前线程获取写锁的数量已经达到最大值,失败。
  6. * 3. 否则,当前线程获得锁
  7. */
  8. Thread current = Thread.currentThread(); //返回当前线程对象
  9. int c = getState();
  10. int w = exclusiveCount(c);
  11. if (c != 0) {
  12. // (Note: if c != 0 and w == 0 then shared count != 0)
  13. if (w == 0 || current != getExclusiveOwnerThread())
  14. return false;
  15. if (w + exclusiveCount(acquires) > MAX_COUNT)
  16. throw new Error("Maximum lock count exceeded");
  17. // Reentrant acquire
  18. setState(c + acquires);
  19. return true;
  20. }
  21. if (writerShouldBlock() ||
  22. !compareAndSetState(c, c + acquires))
  23. return false;
  24. setExclusiveOwnerThread(current);
  25. return true;
  26. }

3.2.2 tryRelease方法

释放方法很简单,就是对锁的状态进行判断,无误则对状态进行修改。

  1. protected final boolean tryRelease(int releases) {
  2. if (!isHeldExclusively())
  3. throw new IllegalMonitorStateException();
  4. int nextc = getState() - releases;
  5. boolean free = exclusiveCount(nextc) == 0;
  6. if (free)
  7. setExclusiveOwnerThread(null);
  8. setState(nextc);
  9. return free;
  10. }

isHeldExclusively()方法

读锁是否被当前线程持有

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

getExclusiveOwnerThread()方法

获取锁持有者线程

  1. protected final Thread getExclusiveOwnerThread() {
  2. return exclusiveOwnerThread;
  3. }

3.3 读锁的获取与释放

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

3.3.1 tryAcquireShared方法

        当写锁被另外一个线程持有时,获取读锁失败;否则调用readerShouldBlock()来判断这次获取读锁的操作应不应该阻塞,然后判断读锁数是否达到上限,如果上面两次判断都通过,就调用compareAndSetState()来更新锁状态。

        readerShouldBlock()方法主要是为了避免尝试获取写锁的线程陷入无限阻塞,如果队列的头结点是一个等待的writer节点,那么该方法就会返回true。

        通过下面代码可以看到,在调用compareAndSetState()更新锁状态成功后,会进入if方法体。这段代码的作用主要是:更新第一个获取读锁的线程指针和获取次数(firstReader 和firstReaderHoldCount),更新上一个获取读锁的线程指针(cachedHoldCounter),最后更新当前线程获取读锁的数量(通过ThreadLocal存在自己线程的容器中)。若读锁数量为0,表示当前线程是第一个,执行下面1处代码;若当前线程是第一个,然后现在再次获取了,执行下面2处代码;否则更新当前线程的获取读锁数和cachedHoldCounter,最后获取读锁数都+1。最后返回1。

  1. protected final int tryAcquireShared(int unused) {
  2. /*
  3. * Walkthrough:
  4. * 1. 如果写锁被另一个线程持有,则获取读锁失败。
  5. * 2. 否则调用readerShouldBlock()来判断这次获取读锁的操作
  6. * 应不应该阻塞,然后判断读锁数是否达到上限。
  7. * 3. 如果上面两次判断都通过,就调用compareAndSetState()来更新锁状态。
  8. */
  9. Thread current = Thread.currentThread();
  10. int c = getState();
  11. if (exclusiveCount(c) != 0 &&
  12. getExclusiveOwnerThread() != current) // 写锁被其它线程持有
  13. return -1;
  14. int r = sharedCount(c); // 读锁数量
  15. if (!readerShouldBlock() &&
  16. r < MAX_COUNT &&
  17. compareAndSetState(c, c + SHARED_UNIT)) { // 读锁+1
  18. if (r == 0) {
  19. firstReader = current;
  20. //1、当前线程是第一个获取读锁的线程
  21. firstReaderHoldCount = 1;
  22. } else if (firstReader == current) {
  23. //2、当前线程是第一个,然后现在再次获取了
  24. firstReaderHoldCount++;
  25. } else {
  26. //3、更新当前线程的获取读锁数,然后更新cachedHoldCounter
  27. HoldCounter rh = cachedHoldCounter;
  28. if (rh == null || rh.tid != getThreadId(current))
  29. cachedHoldCounter = rh = readHolds.get();
  30. else if (rh.count == 0)
  31. readHolds.set(rh);
  32. rh.count++;
  33. }
  34. return 1;
  35. }
  36. return fullTryAcquireShared(current);
  37. }

3.3.2 tryReleaseShared

        首先更新firstReader,然后更新cachedHoldCounter和当前线程的锁数量,最后更新锁的状态。同时若读写锁数量都为0,则返回true,这是为了上层方法唤醒阻塞的writer线程;否则返回false。

  1. protected final boolean tryReleaseShared(int unused) {
  2. Thread current = Thread.currentThread();//获取当前线程
  3. if (firstReader == current) {
  4. // 当前线程是第一个获取读锁的线程
  5. if (firstReaderHoldCount == 1)
  6. firstReader = null;
  7. else
  8. firstReaderHoldCount--;
  9. } else {
  10. HoldCounter rh = cachedHoldCounter; // 上一个获取读锁的线程
  11. if (rh == null || rh.tid != getThreadId(current))
  12. rh = readHolds.get(); // rh指向当前线程获取读锁的数量对象
  13. int count = rh.count; // 当前线程获取读锁的数量
  14. if (count <= 1) {
  15. readHolds.remove();
  16. if (count <= 0)
  17. throw unmatchedUnlockException();
  18. }
  19. --rh.count; // 若当前线程读锁数>1,则数量-1
  20. }
  21. for (;;) {
  22. int c = getState();
  23. int nextc = c - SHARED_UNIT; // 读写锁的数量
  24. if (compareAndSetState(c, nextc)) // 更新锁状态
  25. // 若读写锁数量都为0,则返回true,这里是为了唤醒阻塞的writer线程
  26. return nextc == 0;
  27. }
  28. }

4、公平锁和非公平锁

        与ReentrantLock一样,ReentrantReadWriteLock也分为公平锁和非公平锁,可以在构造方法中指定,不指定默认就是非公平锁。公平锁和非公平锁都继承自内部同步器Sync,它们的区别仅在writerShouldBlock()和readerShouldBlock()这两个方法的实现上。writerShouldBlock()在tryAcquire()方法中即获取写锁的时候会调用,readerShouldBlock()在tryAcquireShared()方法中即获取写锁的时候会调用。

  1. public ReentrantReadWriteLock(boolean fair) {
  2. sync = fair ? new FairSync() : new NonfairSync();
  3. readerLock = new ReadLock(this);
  4. writerLock = new WriteLock(this);
  5. }

4.1 使用原理

        就是在构造方法的时候给sync属性传入FairLock或NonFairLock对象,然后接下来就用sync来进行锁的操作了。

  1. public class ReentrantReadWriteLock
  2. implements ReadWriteLock, java.io.Serializable {
  3. final Sync sync; // 构造方法中初始化成FairLock或NonFairLock,进行lock和unlock的主要对象
  4. public ReentrantReadWriteLock(boolean fair) {
  5. sync = fair ? new FairSync() : new NonfairSync();
  6. readerLock = new ReadLock(this);
  7. writerLock = new WriteLock(this);
  8. }
  9. public ReentrantReadWriteLock() { this(false); }
  10. // 自定义同步器,tryAcquire()、tryRelease()等主要方法都在里面实现了
  11. abstract static class Sync extends AbstractQueuedSynchronizer {...}
  12. // 公平锁,就是一个同步器,特别实现readerShouldBlock()和writerShouldBlock()来实现公平锁
  13. static final class FairSync extends Sync {...}
  14. // 非公平锁,就是一个同步器,特别实现readerShouldBlock()和writerShouldBlock()来实现非公平锁
  15. static final class NonfairSync extends Sync {...}

4.2 公平锁

        writerShouldBlock()和readerShouldBlock()都是判断当前节点有没有不是头结点的前驱节点,即是否有线程比当前线程等待更久的时间,有的话返回false,否则true。

  1. static final class FairSync extends Sync {
  2. private static final long serialVersionUID = -2274990926593161451L;
  3. final boolean writerShouldBlock() {
  4. return hasQueuedPredecessors(); // 判断当前节点有没有不是头结点的前驱节点
  5. }
  6. final boolean readerShouldBlock() {
  7. return hasQueuedPredecessors();
  8. }
  9. }
  10. public final boolean hasQueuedPredecessors() {
  11. Node t = tail; // Read fields in reverse initialization order
  12. Node h = head;
  13. Node s;
  14. return h != t &&
  15. ((s = h.next) == null || s.thread != Thread.currentThread());
  16. }

4.3 非公平锁

        获取写锁的线程一定不会阻塞,因为始终返回false。获取读锁的线程,若队列中第一个节点是独占节点,则该线程获取读锁失败,这是为了避免获取写锁一直阻塞。

  1. static final class NonfairSync extends Sync {
  2. private static final long serialVersionUID = -8159625535654395037L;
  3. final boolean writerShouldBlock() {
  4. return false; // writers can always barge
  5. }
  6. final boolean readerShouldBlock() {
  7. return apparentlyFirstQueuedIsExclusive(); // 队列中第一个节点是否是writer节点
  8. }
  9. }

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/运维做开发/article/detail/956133
推荐阅读
相关标签
  

闽ICP备14008679号