目录
前言
之前写过《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个节点是写锁,返回 truer < MAX_COUNT
指读锁的数量,没有达到极限compareAndSetState(c, c + SHARED_UNIT)
读锁数量加1。
当这三个条件都符合时,返回 1,即拿到读锁,方法结束。
2. 2 fullTryAcquireShared
fullTryAcquireShared(current)
这个方法是自旋,直到返回 1 或是 -1,不再展开细说。
稍微总结下,读锁的逻辑很简单,大致分两种情况:
- 其它线程持有写锁,获取读锁会入队阻塞
- 其它线程没有持有写锁,在 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
这个字段设置为当前线程,即持有写锁的线程,方法结束。
写锁加锁逻辑,小结如下:
- 有读锁未释放,或是其它线程持有写锁时,加锁阻塞。
- 无锁状态,或是锁重入时,修改
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
,逻辑很清晰
- 判断是否持有写锁,若没有抛出异常
- 持有写锁的状态下,将
state
减 1 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
这个方法,返回某线程持有读锁数量,记录方式有两种。
- 首个持有读锁的线程
- 其它线程
它们记录读锁的方式,完全不一样,先说简单的,
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
读写锁的相关逻辑,
锁的逻辑是比较简单的,只有读锁计数这个复杂些。
对于读多写少场景下,使用锁的话,读写锁性能肯定优于普通锁。
大概扯到这儿吧,收工!