ReentrantReadWriteLock到底怎么玩儿?


前言

之前写过《ReentrantLock 源码解析》,对公共资源访问时,加锁可以解决并发问题。

比如对于一个缓存系统,若要求修改缓存时,读请求等待,使其读到的一定是最新值。

简单实现的话,可以设置个全局锁,修改缓存时,先获取锁,修改后再释放锁。

读请求时,也先获取锁,获取失败,说明正在修改缓存。这样就可以满足上面的要求。

但细想下,虽然读写串行,但附带的,读请求与读请求之间,也是串行。

若短时间内,有大量读请求,串行化处理,性能显然不咋的。

能不能使读写之间互斥,读与读之间共享呢?

当然是可以的,用 ReentrantReadWriteLock 就可以实现


提示:本篇基于 JDK 8 展开讨论

一、读写锁极简 Demo


  private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

  private final Map<String, String> map = new HashMap();

  public String getCach(String key) {
      readWriteLock.readLock().lock();
      try {
          return map.get(key);
      } finally {
          readWriteLock.readLock().unlock();
      }
  }

  public String updateCach(String key, String value) {
      readWriteLock.writeLock().lock();
      try {
          return map.put(key, value);
      } finally {
          readWriteLock.writeLock().unlock();
      }
  }

这个demo就满足上文说的读写缓存的要求,本文就重点分析下读写锁的如何实现的。

先把结论说下,之后一条一条的解释:

同一个线程:

  • 读锁未释放时,申请写锁会阻塞,申请读锁不受影响
  • 写锁未释放时,可以申请写锁,也可以申请读锁

不同线程之间:

  • 读锁未释放时,申请写锁会阻塞,申请读锁不受影响
  • 写锁未释放时,申请写锁会阻塞,申请读锁也阻塞

二、读锁源码分析

1. 初始化

代码如下(示例):

public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {

    private final ReentrantReadWriteLock.ReadLock readerLock;

    private final ReentrantReadWriteLock.WriteLock writerLock;

    final Sync sync;
}

ReentrantReadWriteLock 类中有三个属性,不多解释。

初始化时,默认是非公平锁,本文以非公平锁为例来说明


  ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
  
  public ReentrantReadWriteLock() {
      this(false);
  }

  public ReentrantReadWriteLock(boolean fair) {
      sync = fair ? new FairSync() : new NonfairSync();
      readerLock = new ReadLock(this);
      writerLock = new WriteLock(this);
  }

初始化时, readerLock, writerLock, sync 都赋值了。

2. readLock().lock() 获取读锁

代码如下(示例):

	
	public void lock() {
	    sync.acquireShared(1);
	}
	
	public final void acquireShared(int arg) {
	    if (tryAcquireShared(arg) < 0)
	        doAcquireShared(arg);
	}
	
	protected final int tryAcquireShared(int unused) {
	    Thread current = Thread.currentThread();
	    int c = getState();
	    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);
	}

ReentrantReadWriteLock 是基于 AQS 实现的,重写了 tryAcquireShared 方法,

tryAcquireShared 返回 1 时,整个获取读锁的方法就结束了,即获取到了读锁。
tryAcquireShared 返回 -1 时,执行 doAcquireShared(arg),即入队阻塞,等待唤醒。

本文主要讲 tryAcquireShared 方法,其它的入队、阻塞的代码不讲,

有兴趣的话,可以看《ReentrantLock 源码解析

2. 1 tryAcquireShared

tryAcquireShared 这个方法简化如下


protected final int tryAcquireShared(int unused) {
    Thread current = Thread.currentThread();
    int c = getState();
    if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
        return -1;
    int r = sharedCount(c);
    if (!readerShouldBlock() &&
        r < MAX_COUNT &&
        compareAndSetState(c, c + SHARED_UNIT)) {
		// 读锁计数相关代码,先略去,不影响主流程
        return 1;
    }
    return fullTryAcquireShared(current);
}

某些条件下返回 -1, 执行 acquireShared 方法,请求读锁的线程,入队阻塞。

某些条件下返回 1,获取读锁成功,方法结束。

顺便说一句:c 的低16位,存的是写锁的数量,高16位存的是读锁的数量。

  • exclusiveCount(c) != 0 这句指的是 c 的低16位是否有值,即是否存在写锁
  • if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current) ,即其它线程持有写锁

第 1 个 if 翻译下,就是其它线程持有写锁时,读锁请求 入队阻塞。

int r = sharedCount(c) 是取 c 的高16位,即当前读锁的数量。

  • readerShouldBlock() 非公平锁实现:等待队列中,第1个节点是写锁,返回 true
  • r < MAX_COUNT 指读锁的数量,没有达到极限
  • compareAndSetState(c, c + SHARED_UNIT) 读锁数量加1。
    当这三个条件都符合时,返回 1,即拿到读锁,方法结束。

2. 2 fullTryAcquireShared

fullTryAcquireShared(current) 这个方法是自旋,直到返回 1 或是 -1,不再展开细说。

稍微总结下,读锁的逻辑很简单,大致分两种情况:

  1. 其它线程持有写锁,获取读锁会入队阻塞
  2. 其它线程没有持有写锁,在 c 的高16位加1,也就是拿到了读锁。

中间略去了一段代码没说,那是处理线程上读锁计数的,等会儿再说。

3. readLock().unlock() 释放读锁


 public void unlock() {
     sync.releaseShared(1);
 }
 public final boolean releaseShared(int arg) {
     if (tryReleaseShared(arg)) {
         doReleaseShared();
         return true;
     }
     return false;
 }

 protected final boolean tryReleaseShared(int unused) {
     Thread current = Thread.currentThread();
     if (firstReader == current) {
         // assert firstReaderHoldCount > 0;
         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 (;;) {
         int c = getState();
         int nextc = c - SHARED_UNIT;
         if (compareAndSetState(c, nextc))
             // Releasing the read lock has no effect on readers,
             // but it may allow waiting writers to proceed if
             // both read and write locks are now free.
             return nextc == 0;
     }
 }

读锁解锁代码,关于 AQS 的,不展开讲,只说重写的方法 tryReleaseShared

其它的逻辑,可以看 《ReentrantLock 源码解析


protected final boolean tryReleaseShared(int unused) {
	// 读锁计数相关代码,先略去,不影响主流程
   for (;;) {
       int c = getState();
       int nextc = c - SHARED_UNIT;
       if (compareAndSetState(c, nextc))
           // Releasing the read lock has no effect on readers,
           // but it may allow waiting writers to proceed if
           // both read and write locks are now free.
           return nextc == 0;
   }
}

这是个自旋,直到 c 的高16位成功减1,然后退出

读锁与读锁之间不互斥,从代码中明显可以看出,只是对 c 的高16位操作而已。

读锁与写锁互斥exclusiveCount(c) != 0 是这个条件判断决定的。

三、写锁源码分析

1 writeLock().lock() 获取写锁

	public void lock() {
	   sync.acquire(1);
	}
	   
	public final void acquire(int arg) {
	   if (!tryAcquire(arg) &&
	       acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
	       selfInterrupt();
	}
	
	protected final boolean tryAcquire(int acquires) {
	   Thread current = Thread.currentThread();
	   int c = getState();
	   int w = exclusiveCount(c);
	   if (c != 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;
	}
    

获取写锁的代码,关于 AQS 的,不展开讲,大概的流程就是,

tryAcquire 获取写锁成功,那就方法结束,失败就执行 addWaiter 入队,

再执行 acquireQueued ,即入队后阻塞。相关逻辑看 《ReentrantLock 源码解析

这里详细说说重写的方法 tryAcquire

1. 1 c != 0

int w = exclusiveCount(c); 这个指的是写锁的数量

c != 0 指 有锁未释放,高16位是读锁,低16位写锁,都有可能。

c != 0 and w == 0 这个意思是有读锁未释放
c != 0 and w != 0 and current != getExclusiveOwnerThread() 这个意思是其它线程持有写锁

也就是说,c != 0 时,有读锁未释放,或是 其它线程持有写锁时,会入队阻塞。

除此之外,只有一种情况,就是线程自身持有写锁,这种情况下加锁成功,方法结束。

为什么这里 setState 没有用 CAS?
因为这里不可能有并发,只有当前线程持有写锁这种情况下才执行这行,不会并发。

1. 2 c == 0

没有读锁与没有写锁,此时以 CAS 的方式,给 c 加 1,失败就阻塞,成功即拿到了锁。

exclusiveOwnerThread 这个字段设置为当前线程,即持有写锁的线程,方法结束。

写锁加锁逻辑,小结如下:

  1. 有读锁未释放,或是其它线程持有写锁时,加锁阻塞。
  2. 无锁状态,或是锁重入时,修改 state, 设置 exclusiveOwnerThread,加锁成功。

2 writeLock().unlock() 释放写锁


	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;
	}
	
	protected final boolean tryRelease(int releases) {
	    if (!isHeldExclusively())
	        throw new IllegalMonitorStateException();
	    int nextc = getState() - releases;
	    boolean free = exclusiveCount(nextc) == 0;
	    if (free)
	        setExclusiveOwnerThread(null);
	    setState(nextc);
	    return free;
	}

unparkSuccessor(h) 这个是唤醒CLH队列中的节点,具体可参阅《ReentrantLock 源码解析

大概看下重写的方法 tryRelease ,逻辑很清晰

  1. 判断是否持有写锁,若没有抛出异常
  2. 持有写锁的状态下,将 state 减 1
  3. state 如果等于 0,即写锁完全释放,清除字段 exclusiveOwnerThread

至此读写锁的大概逻辑讲完了,足可以应对面试了。

中间讲解读锁时,关于锁计数的内容略去了。

如果没兴趣,下面的内容可以不看,也不太重要。

四、读锁的计数原理

1 读锁的总数

c 的高16位,记录了读锁的总数。


 final int getReadLockCount() {
     return sharedCount(getState());
 }
 
 static final int SHARED_SHIFT   = 16;
 
 static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
 

getReadLockCount 这个方法,可以获取读锁的数量,就是 c 的高16位

2 单个线程持有的读锁数量

 final int getReadHoldCount() {
     if (getReadLockCount() == 0)
         return 0;

     Thread current = Thread.currentThread();
     if (firstReader == current)
         return firstReaderHoldCount;

     HoldCounter rh = cachedHoldCounter;
     if (rh != null && rh.tid == getThreadId(current))
         return rh.count;

     int count = readHolds.get().count;
     if (count == 0) readHolds.remove();
     return count;
 }

getReadHoldCount 这个方法,返回某线程持有读锁数量,记录方式有两种。

  1. 首个持有读锁的线程
  2. 其它线程

它们记录读锁的方式,完全不一样,先说简单的,

2. 1 首个持有读锁的线程

sync 这个对象里,有两个属性


  // 记录首个拿到读锁的线程
  private transient Thread firstReader = null;

  // 记录首个拿到读锁的线程,一共持有多少个读锁
  private transient int firstReaderHoldCount;

再把获取读锁的代码拿过来,只看计数相关的


  protected final int tryAcquireShared(int unused) {
      Thread current = Thread.currentThread();
      int c = getState();
	  // 省略相关代码
      int r = sharedCount(c);
      if (// 省略相关条件) {
          if (r == 0) {
              firstReader = current;
              firstReaderHoldCount = 1;
          } else if (firstReader == current) {
              firstReaderHoldCount++;
          } else {
			// 省略相关代码
          }
          return 1;
      }
      return fullTryAcquireShared(current);
  }

r == 0 即无锁状态,这时候对 firstReader、firstReaderHoldCount 赋值。

也就是记录了首个获取读锁的线程,持有读锁的数量,都记录了,锁重入时,数量加1。

2. 1 其它线程

在高并发场景下,别的线程持有读锁时,

此时也可以获取读锁,并且会记录各线程持有读锁的数量。

tryAcquireShared 方法中,这段代码就是记录数量的。

} 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++;
}

别觉得这几行代码短,可能这是 ReentrantReadWriteLock 中最让人费解的代码了。

相关属性的代码,在下面了, readHolds 本质就是 ThreadLocal


  abstract static class Sync extends AbstractQueuedSynchronizer {
      static final class HoldCounter {
          int count = 0;
          // Use id, not reference, to avoid garbage retention
          final long tid = getThreadId(Thread.currentThread());
      }
      
      static final class ThreadLocalHoldCounter
          extends ThreadLocal<HoldCounter> {
          public HoldCounter initialValue() {
              return new HoldCounter();
          }
      }
      private transient ThreadLocalHoldCounter readHolds;
      
      private transient HoldCounter cachedHoldCounter;
      
      Sync() {
          readHolds = new ThreadLocalHoldCounter();
          setState(getState()); // ensures visibility of readHolds
      }
 }

new ReentrantReadWriteLock() 实例化时,Sync 执行构造方法,readHolds 被赋值

rh == null || rh.tid != getThreadId(current) 条件为 true 时,执行 readHolds.get()

这里用的是 ThreadLocal ,如果对这个不熟悉,可参阅《ThreadLocal 原理


 public T get() {
     Thread t = Thread.currentThread();
     ThreadLocalMap map = getMap(t);
     if (map != null) {
		// map 不为空时,代码省略,本例里 map 为空
     }
     return setInitialValue();
 }
  private T setInitialValue() {
      T value = initialValue(); // 会调用重写的方法
      Thread t = Thread.currentThread();
      ThreadLocalMap map = getMap(t);
      if (map != null)
          map.set(this, value);
      else
          createMap(t, value);
      return value;
  }

调用 get() 方法时,会调用重写的 initialValue(),之后会被 set 进去。

在这里插入图片描述
这样通过 ThreadLocal 线程内部,会有一个 HoldCounter 对象,来保存该线程持有的读锁数量。

如果你还觉得懵,那不要怀疑自己,ThreadLocal 不好懂,回头有空再慢慢看吧。

每个线程各自有自己的 HoldCounter 对象,没有就 new 一个,

获取读锁时 count 加 1,释放读锁时 count 减1,以此来维护读锁数量。

你有没有这么个疑问,为什么会有两种机制呢?

我猜测,大神 Doug Lea 用两种机制,代码是复杂些,应该是从性能考虑的。

假设读锁竞争不激烈,那第一种机制,速度肯定快。

如果读锁竞争很激烈,那第二种机制,保证数据的准确性。

为了极至的性能,牺牲一点代码的可读性,还是可以的,毕竟是 JDK 的工具类呀!


总结

本文大概讲了 ReentrantReadWriteLock 读写锁的相关逻辑,

锁的逻辑是比较简单的,只有读锁计数这个复杂些。

对于读多写少场景下,使用锁的话,读写锁性能肯定优于普通锁。

大概扯到这儿吧,收工!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值