并发编程篇:读写锁(ReentrantReadWriteLock)的源码分析

x33g5p2x  于2021-12-18 转载在 其他  
字(3.0k)|赞(0)|评价(0)|浏览(442)

定义

1、读写锁在同一时刻有多个读线程访问,在写线程时候所有的读锁和其他写线程都被阻塞。读写锁维护了一个读锁和一个写锁,通过分离读锁和写锁,并发性比其他排它锁都提升了。它具备3个特性:

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

读写锁的实现分析

1、读写锁的状态设计

使用一个整形编码维护读写状态,按位切割使用这个变量,其中高16位表示读,低16位表示写,如下图

1、获取状态:如果当前同步状态是S,写状态为 S & 0x0000FFFF(将高16位全部抹去),读状态为S >>> 16(无符号补0右移16位)
2、当写状态增加1是,等于S+1;读状态增加1是,等于S+(1<< 16)
3、当S不等于0时,当写状态(S & 0xooooFFFF)等于0时,则读状态(S>>>16)大于0,即读锁已被获取

写锁的获取与释放

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

  1. protected final boolean tryAcquire(int acquires) {
  2. Thread current = Thread.currentThread();
  3. //获取锁的状态
  4. int c = getState();
  5. //获取写锁的状态
  6. int w = exclusiveCount(c);
  7. if (c != 0) {
  8. // 存在读锁或者当前线程不是已获取读写锁的线程
  9. if (w == 0 || current != getExclusiveOwnerThread())
  10. return false;
  11. if (w + exclusiveCount(acquires) > MAX_COUNT)
  12. throw new Error("Maximum lock count exceeded");
  13. // Reentrant acquire
  14. setState(c + acquires);
  15. return true;
  16. }
  17. if (writerShouldBlock() || !compareAndSetState(c, c + acquires))
  18. return false;
  19. setExclusiveOwnerThread(current);
  20. return true;
  21. }

获取写锁的公平和非公平如何保证

读锁的获取与释放

1、读锁是一个支持重进入的共享锁,能够被多个线程同时获取,在没有其他写线程访问时候,读锁总是可以被重新获取的。
2、当前线程重新获取读锁,则增加读的状态。
3、如果当前线程在获取读锁时候,其他线程获取了写锁,则进入等待状态

  1. */
  2. Thread current = Thread.currentThread();
  3. int c = getState();
  4. if (exclusiveCount(c) != 0 &&
  5. getExclusiveOwnerThread() != current)
  6. return -1;
  7. int r = sharedCount(c);
  8. if (!readerShouldBlock() &&
  9. r < MAX_COUNT &&
  10. compareAndSetState(c, c + SHARED_UNIT)) {
  11. if (r == 0) {
  12. firstReader = current;
  13. firstReaderHoldCount = 1;
  14. } else if (firstReader == current) {
  15. firstReaderHoldCount++;
  16. } else {
  17. HoldCounter rh = cachedHoldCounter;
  18. if (rh == null || rh.tid != getThreadId(current))
  19. cachedHoldCounter = rh = readHolds.get();
  20. else if (rh.count == 0)
  21. readHolds.set(rh);
  22. rh.count++;
  23. }
  24. return 1;
  25. }
  26. return fullTryAcquireShared(current);
  27. }
公平获取锁和非公平获取锁

公平锁看是否有前驱节点

  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. }

非公平锁,直接返回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();
  8. }
  9. }

锁降级

锁的降级,是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程

  1. //读写锁
  2. private ReentrantReadWriteLock lock=new ReentrantReadWriteLock();
  3. //读锁
  4. private ReadLock readLock=lock.readLock();
  5. //写锁
  6. private WriteLock writeLock=lock.writeLock();
  7. private boolean update;
  8. public void processData(){
  9. //读锁获取
  10. readLock.lock();
  11. if(!update){
  12. //必须先释放读锁
  13. readLock.unlock();
  14. //锁降级从获取写锁开始
  15. writeLock.lock();
  16. try {
  17. if(!update){
  18. //准备数据流程(略)
  19. update=true;
  20. }
  21. //获取读锁。在写锁持有期间获取读锁
  22. //此处获取读锁,是为了防止,当释放写锁后,又有一个线程T获取锁,对数据进行改变,而当前线程下面对改变的数据无法感知。
  23. //如果获取了读锁,则线程T则被阻塞,直到当前线程释放了读锁,那个T线程才有可能获取写锁。
  24. readLock.lock();
  25. }finally{
  26. //释放写锁
  27. writeLock.unlock();
  28. }
  29. //锁降级完成
  30. }
  31. try {
  32. //使用数据的流程
  33. } finally{
  34. //释放读锁
  35. readLock.unlock();
  36. }
  37. }

锁降级的必要性:主要为了保证数据的可见性。如果当前线程不获取读锁而是直接释放写锁,假设此刻另一个线程(T)获取了写锁并修改了数据,那么当前线程无法感知线程T的数据更新。如果当前线程获取读锁,即遵循锁的降级步骤,则线程T将会被阻塞,直到线程使用数据并释放读锁之后,线程T才能获取读写锁更新数据。

相关文章