【JAVA并发编程】读写锁ReentrantReadWriteLock的实现分析

一、简介 读写锁在同一时刻允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了两把锁,一把读锁和一把写锁。获取读写锁可分为下面两种情况:

  • 同一线程:该线程获取读锁后,能够再次获取读锁,但不能获取写锁。该线程获取写锁后,能够再次获取写锁,也可以再获取读锁。
  • 不同线程:A线程获取读锁后,B线程可以再次获取读锁,不可以获取写锁。A线程获取写锁后,B线程无法获取读锁和写锁。
二、读写锁示例
public class Cache { static Map map = new HashMap(); static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); static Lock r = rwl.readLock(); static Lock w = rwl.writeLock(); public static final Object get(String key) { r.lock(); try { return map.get(key); } finally { r.unlock(); } } public static final Object put(String key, Object value) { w.lock(); try { return map.put(key, value); } finally { w.unlock(); } } }

上述示例中,Cache组合一个非线程安全的HashMap作为缓存的实现,同时使用读写锁来保证Cache是线程安全的。
三、读写锁的实现分析 3.1 读写状态的设计
回想AQS的实现,同步状态表示锁被获取的次数,而读写锁的自定义同步器需要在同步状态(一个整型变量)上维护多个读线程和一个写线程的状态,该状态的设计成为设计读写锁的关键。
【【JAVA并发编程】读写锁ReentrantReadWriteLock的实现分析】如果在一个整型变量上维护多种状态,就需要“按位切割使用”这个变量,读写锁将变量切分成了两部分,高16位表示读,低16位表示写,划分方式如下图。
【JAVA并发编程】读写锁ReentrantReadWriteLock的实现分析
文章图片

上图同步状态表示一个线程已经获取了写锁,且重进入了两次,同时也连续获取了两次读锁。读写锁时如何确定读和写的状态?答案是通过位运算。假设当前同步状态为S,写状态等于 S & 0x0000FFFF(将高16位抹去),读状态等于S >>> 16(无符号补0右移16位)。当写状态增加1时,等于S + 1,当读状态增加1时,等于S + (1 << 16),也就是 S + 0x00010000。
3.2 写锁的获取与释放
写锁是一个支持重入的排它锁。如果当前线程已经获取了写锁,则增加写状态。如果当前线程在获取写锁时,读锁已经被获取(读状态不为0)或者该线程不是已经获取写锁的线程,则当前线程进入等待状态。代码如下
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; }

如果存在读锁(即使只有当前线程获取了读锁也不行),则写锁不能被获取,原因在于:读写锁要确保写锁的操作对读锁可见,因此只有等待其他读线程都释放了读锁,写锁才能被当前线程获取,而写锁一旦获取,其他读写线程的后续访问均被阻塞。
3.3 读锁的获取与释放
读锁是一个支持重入的共享锁,它能够被多个线程同时获取,在写状态为0时,读锁总会被成功获取,而所做的也只是增加读状态。如果当前线程在获取读锁时,写锁已被其他线程获取,则进入等待状态。
protected final int tryAcquireShared(int unused) { for (; ; ) { int c = getState(); int nextc = c + (1 << 16); if (nextc < c) throw new Error("Maximum lock count exceeded"); if (exclusiveCount(c) != 0 && owner != Thread.currentThread()) return -1; if (compareAndSetState(c, nextc)) return 1; } }

如果其他线程已经获取了写锁,则获取读锁失败,进入等待状态。
读锁的每次释放均减少读状态,减少的值是1 << 16。
3.4 锁降级
锁降级指的是写锁降级成读锁。锁降级是持有了写锁之后,在获取到读锁,随后释放之前拥有的写锁,那么只剩下读锁,这个过程是锁降级(读写锁不支持锁升级)。代码如下
public void processData() { readLock.lock(); if (!update) { // 必须先释放读锁 readLock.unlock(); // 锁降级从写锁获取开始 writeLock.lock(); try { if (!update) { update = true; } readLock.lock(); } finally { writeLock.unlock(); } // 锁降级完成,写锁降级为读锁 } try { // 使用update } finally { readLock.unlock(); } }

上面代码中锁降级中,读锁的获取是否必要?是必要的,因为修改update之后,后续还要使用到update,所以为了防止其他线程修改update,所以需要加读锁。
四、总结 一般情况下,读写锁的性能优于ReentrantLock,因为大多数场景读是远大于写的,所以在读多于写的情况下,读写锁能够提供更好的并发性和吞吐量。

    推荐阅读