面试:谈谈你对ReentrantReadWriteLock的理解,推荐看看(图解版)

猿天地

共 7351字,需浏览 15分钟

 ·

2020-10-23 22:17

转自:Ccww技术博客


前言

在前面看了ReentrantLock面试分析,在面试问了多并发的就少不了ReentrantReadWriteLock读写锁了,那具体是什么问题?我们来看看:

  • ReentrantReadWriteLock读写锁是什么,有什么用?

    • ReentrantReadWriteLock具有哪些特性?

  • ReentrantReadWriteLock是怎么实现读写锁的获取?

  • ReentrantReadWriteLock读写锁的释放

ReentrantReadWriteLock读写锁是什么,有什么用?

平时存在很多场景,大部分线程提供读服务,只有少数的写服务。然而读服务不存在数据竞争问题,如果一个线程在读时禁止其他线程读势必会导致性能降低,因此延伸了ReadWriteLock读写锁。

ReentrantReadWriteLock读写锁维护着一对锁,一个读锁和一个写锁。

通过分离读锁和写锁,使得对共享资源的访问分为读访问和写访问,多个线程可以同时对共享资源进行读访问,读写锁可以极大地提高并发量。但是同一时间只能有一个线程对共享资源进行写访问,其他所有读线程和写线程都会被阻塞。

ReentrantReadWriteLock具有哪些特性?

读写锁的主要特性:

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

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

  3. 锁降级:遵循获取写锁、获取读锁在释放写锁的次序,写锁能够降级成为读锁

  4. 读写锁除了读读不互斥,读写、写读、写写都是互斥的

    是否互斥

在初步了解了ReentrantReadWriteLock读写锁的原理的情况,我们来讲讲其到底是怎么实现读写锁的?

ReentrantReadWriteLock是怎么实现读写锁的获取?

ReentrantReadWriteLock实现读写锁以及公平性是依赖:

// 读锁public static class ReadLock implements Lock, java.io.Serializable {}// 写锁public static class WriteLock implements Lock, java.io.Serializable {}// 同步器-公平性abstract static class Sync extends AbstractQueuedSynchronizer {}

接下来我们通过构造函数创建一个非公平性的ReentrantReadWriteLock读写锁的来看看其实现原理:

// 默认构造方法*public ReentrantReadWriteLock() {this(false);}
public ReentrantReadWriteLock(boolean fair) { //默认非公平锁 sync = fair ? new FairSync() : new NonfairSync(); //创建读锁 readerLock = new ReadLock(this); //创建写锁 writerLock = new WriteLock(this); }

读写锁ReadLockWriteLock其都是使用同一个Sync

//读锁上锁protected ReadLock(ReentrantReadWriteLock lock) {           sync = lock.sync;      }//写锁上锁    protected WriteLock(ReentrantReadWriteLock lock) {           sync = lock.sync;      }

那我们来看看这个sync有什么奇特之处?

Sync中32位的同步状态state,划分成2部分,高16位表示读,低16位表示写,假设当前同步状态为s:

  • 读状态为 s >>> 16,即无符号补0右移16位,读状态增加1,等于 s + (1 << 16)

  • 写状态为 s & 0000FFFF,即将高16位全部抹去,写状态增加1,等于 s + 1


abstract static class Sync extends AbstractQueuedSynchronizer {       private static final long serialVersionUID = 6317671515068378041L;
static final int SHARED_SHIFT = 16; //读状态加+1使用变量 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;
/** 获取读状态 */ static int sharedCount(int c) { return c >>> SHARED_SHIFT; } /** 获取写状态 */ static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; } ... }

readLock::lock()实现

调用readLock.lock()进行获取读锁共享锁:

public void lock() {           sync.acquireShared(1);      }public final void acquireShared(int arg) {  //尝试获取同步状态       if (tryAcquireShared(arg) < 0)           //获取失败则加入到同步队列           doAcquireShared(arg);  }

尝试获取同步状态tryAcquireShared(arg):

  • 互斥锁-写状态不为0(写锁已被获取),且获取写锁的不是当前线程,那么直接返回失败(已写锁状态-->进入等待队列

  • 判断readerShouldBlock()同步队列中第一个节点是否为写线程的节点,如果是写线程需要阻塞,如果是读线程不应该阻塞,需要更新读状态成功

    • 如果不存在,设置到当前线程在最近获取读锁的线程CachedHoldCounter,且重入次数加+1

    • 存在,重入次数加+1

    • 如果之前还没有线程获取读锁,记录第一个读者为当前线程(无锁状态时-->获取读锁

    • 如果有线程获取了读锁且是当前线程是队列中第一个读线程,则把其重入次数加1(已有读锁时-->线程读锁再重入

    • 如果有线程获取了读锁且当前线程不是队列中第一个读线程,先判断readHolds是否存在当前线程(已有读锁时-->当前线程非首个获取读锁

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);  //如果读线程不应该阻塞,且更新读状态成功,那么返回成功  //读线程是否需要排队(是否是公平模式)           if (!readerShouldBlock() &&               r < MAX_COUNT &&               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 1;          }  //尝试获取读状态           return fullTryAcquireShared(current);      }

CAS更新读状态失败,或者readerShouldBlock()判断出第一个节点为写线程的节点,调用此方法尝试获取读状态

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 (sharedCount(c) == MAX_COUNT)                   throw new Error("Maximum lock count exceeded");               //不给同步队列中的写线程让位,而是直接尝试获取读锁               if (compareAndSetState(c, c + SHARED_UNIT)) {                  ...              }          }      }

获取读锁失败了就可能要排队,其原理跟ReentrantLock差不多。其中setHeadAndPropagate()唤醒下一个读节点,是一个唤醒一个唤醒队列中的读锁,而不是一次性唤醒后面所有的读节点。

这样不是很简洁明了,那直接看看图:

当已写锁状态-->进入等待队列

当为无锁状态时-->获取读锁

当已有读锁时-->读锁再重入

当已有读锁时-->当前线程非首个获取读锁

还有其他复杂的情况,可以自己熟悉后再慢慢理解,现在看完了readLock::lock()实现,我们来研究研究writeLock::lock()实现,你会发现比较简单。


writeLock::lock()实现

获取写锁

public final void acquire(int arg) {//尝试获取同步状态,失败则加入到同步队列中       if (!tryAcquire(arg) &&           acquireQueued(addWaiter(Node.EXCLUSIVE), arg))           selfInterrupt();  }

尝试获取同步状态

  • 获取同步状态state,是否为0,是否已经获取过锁了

    • 写状态-互斥 exclusiveCount(c)==0写状态为0(说明读状态不为0,已获取读锁)或者当前线程current != getExclusiveOwnerThread()(写锁已经被抢占),都不允许再次获取其他线程写锁

    • 当前线程已经获取过写锁,这里是重入了,直接把state加1;

    • state不为0时(说明已获取读/写锁了)

    • state为0时(说明还没有读/写锁了),就尝试更新state的值(非公平模式writerShouldBlock()返回false),该线程设置为占有者

protected final boolean tryAcquire(int acquires) {           //获取当前线程           Thread current = Thread.currentThread();  //获取同步状态           int c = getState();  //获取写状态-互斥           int w = exclusiveCount(c);  //已获取锁了(读/写)           if (c != 0) {               // 如果同步状态不为0,且写状态为0,那么说明               if (w == 0 || current != getExclusiveOwnerThread())                   //如果读状态不为0,或者写锁已经被抢占,那么返回获取写锁失败                   return false;               if (w + exclusiveCount(acquires) > MAX_COUNT)                   throw new Error("Maximum lock count exceeded");               //当前线程已经获取过写锁,这里是重入了,直接把state加1即可               setState(c + acquires);               return true;          }//还没有锁(读/写),更新同步状态且设置该线程为占有者           if (writerShouldBlock() ||               !compareAndSetState(c, c + acquires))               return false;           setExclusiveOwnerThread(current);           return true;      }

失败处理流程跟ReadLock模式一样。也来具体看看流程图示:

当为无锁状态-->获取写锁

当已有写锁-->获取重入写锁

当已有写锁-->不同线程获取写锁

那我们来总结一下获取锁的

小结-获取锁

在获取读锁时:

  • 如果写锁已经被其他线程获取,那么此线程将会加入到同步队列,挂起等待唤醒。

  • 如果写锁没有被其他线程获取,但是同步队列的第一个节点是写线程的节点,那么此线程让位给写线程,挂起等待唤醒

  • 如果获取读锁的线程,已经持有了写锁,那么即使同步队列的第一个节点是写线程的节点,它也不会让位给同步队列中的写线程,而是自旋去获取读锁。因为此时让位会造成死锁

获取写锁时:

  • 如果读锁已经被获取,那么不允许获取写锁。将此线程加入到同步队列,挂起等待唤醒

  • 如果读锁没有被获取,但是写锁已经被其他线程抢占,那么还是将此线程加入到同步队列,挂起等待唤醒

  • 如果写锁已经被此线程持有,那么重入,即写状态+1

  • 如果读锁和写锁都没有被获取,那么CAS尝试获取写锁

获取读写锁的研究完,我们也需要看看锁的释放

ReentrantReadWriteLock读写锁的释放

readLock.unlock()

读锁释放步骤

  • 释放锁

    • 如果第一个读线程是当前线程,将重入的次数减1,当减到0了就把第一个读者置为空

    • 如果第一个读线程不是当前线程,也需要将重入的次数减1,

    • 共享锁获取的次数减1, 如果减为0了说明完全释放了,才返回true

  • 释放成功,唤醒下一个节点

// java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock.unlockpublic void unlock() {   sync.releaseShared(1);}// java.util.concurrent.locks.AbstractQueuedSynchronizer.releaseSharedpublic final boolean releaseShared(int arg) {   // 尝试释放锁   if (tryReleaseShared(arg)) {       // 唤醒下一个节点       doReleaseShared();       return true;  }   return false;}// java.util.concurrent.locks.ReentrantReadWriteLock.Sync.tryReleaseSharedprotected final boolean tryReleaseShared(int unused) {   Thread current = Thread.currentThread();   if (firstReader == current) {       // 第一个读线程是当前线程       if (firstReaderHoldCount == 1)           firstReader = null;       else           firstReaderHoldCount--;  } 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;  }   for (;;) {       // 共享锁获取的次数减1       int c = getState();       int nextc = c - SHARED_UNIT;       if (compareAndSetState(c, nextc))           return nextc == 0;  }}
private void doReleaseShared() { for (;;) { Node h = head; if (h != null && h != tail) { int ws = h.waitStatus; // 如果头节点状态为SIGNAL,说明要唤醒下一个节点 if (ws == Node.SIGNAL) { if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) continue; // loop to recheck cases // 唤醒下一个节点 unparkSuccessor(h); } else if (ws == 0 && // 把头节点的状态改为PROPAGATE成功才会跳到下面的if !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; // loop on failed CAS } // 如果唤醒后head没变,则跳出循环 if (h == head) // loop if head changed break; }}


WriteLock.unlock()

写锁释放步骤:

  • 释放锁

    • state状态变量的值减1,是否完全释放锁,如果完全释放锁则设置独占为null

    • 设置state状态变量的值

  • 释放成功,唤醒下一个节点

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;}// java.util.concurrent.locks.ReentrantReadWriteLock.Sync.tryRelease()protected final boolean tryRelease(int releases) {   // 如果写锁不是当前线程占有着,抛出异常   if (!isHeldExclusively())       throw new IllegalMonitorStateException();   // 状态变量的值减1   int nextc = getState() - releases;   // 是否完全释放锁   boolean free = exclusiveCount(nextc) == 0;   if (free)       setExclusiveOwnerThread(null);   // 设置状态变量的值   setState(nextc);   // 如果完全释放了写锁,返回true   return free;

总结

ReentrantReadWriteLock在使用中也会,比如并发的读多写少情景,本地缓存也可以使用其实现,有时间会写一篇关于本地缓存设计方面的文章。


后台回复 学习资料 领取学习视频


如有收获,点个在看,诚挚感谢

浏览 60
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报