一、简介
在并发场景中用于解决线程安全的问题,我们会高频率的使用到独占式锁,也就是关键字 synchronized 或者 JUC 包中实现了 Lock 接口的 ReentrantLock 。它们都是独占式获取锁,也就是在同一时刻只有一个线程能够获取锁。
而在一些业务场景中,大部分只是读数据,写数据很少,如果仅仅是读数据的话并不会影响数据正确性(出现脏读),而如果在这种业务场景下,依然使用独占锁的话,很显然这将是出现性能瓶颈的地方。
针对这种读多写少的情况,java还提供了另外一个实现 Lock 接口的 ReentrantReadWriteLock(读写锁):
- 读写锁允许同一时刻被多个读线程访问;
- 而在写线程访问时,所有的读线程和其他的写线程都会被阻塞。
在分析 WirteLock 和 ReadLock 的互斥性时可以按照 写写之间,读写之间以及读读之间 进行,总体特性如下:
- 公平性选择:支持非公平性(默认)和公平的锁获取方式,吞吐量还是非公平优于公平;
- 可重入性:支持重入,读锁获取后能再次获取,写锁获取之后能够再次获取写锁,同时也能够获取读锁;
- 锁降级:遵循获取写锁,获取读锁再释放写锁的次序,写锁能够降级成为读锁。
二、源码分析
类图如下:
通过上面的类图可以看到,ReentrantReadWriteLock 与 ReentrantLock 的内部类的结构相似,内部类包含在 Sync、NonfairSync、FairSync 、WriteLock 和 ReadLock ,NonfairSync 与 FairSync 类继承自 Sync 类,Sync 类继承自 AbstractQueuedSynchronizer 抽象类。同时 ReentrantReadWriteLock 自身实现了 ReadWriteLock 接口的 readLock() 和 writeLock() 方法。
2.1 状态变量 state
在前面 AQS 一文中我们知道,AQS系列的锁都是通过 state 变量来维护锁状态的。
ReentranReadWriteLock 中也一样,它使用一个 int state 变量同时来维护读锁和写锁的状态。int 类型为 4 字节 32 位,用 state 的高 16 位来存储读锁的状态,用低 16 位来存储写锁的状态。由于16位最大全1表示为65535,所以读锁和写锁最多可以获取65535个。主要涉及变量如下:
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
...
/**
* The synchronization state.
*/
private volatile int state;
protected final int getState() {
return state;
}
...
}
abstract static class Sync extends AbstractQueuedSynchronizer {
...
//偏移位数
static final int SHARED_SHIFT = 16;
//读锁计数基本单位,1左移16位,即实际是65536
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
//读锁、写锁可重入最大数量,即最大值为65535
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
//获取低16位的条件
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; }
...
}
图示的32位二进制数据为:0000000000000010 0000000000000011
2.1.1 读位运算
读位运算涉及的变量和方法如下:
//偏移位数
static final int SHARED_SHIFT = 16;
//读锁计数基本单位
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
//获取读锁的数量
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
读锁使用高16位,每次获取读锁成功+1,所以读锁计数基本单位1左移16位(1
要获取读锁重入数,使用的是 shareCount() 方法,该方法把 state 做无符号右移16位获取读锁的重入数。
//开始没人获取读锁时,state为
0000000000000000 0000000000000000
//来了一个线程获取读锁,state为 原值+SHARED_UNIT,结果为
0000000000000001 0000000000000000
//又来了1个线程获取读锁,state为
0000000000000010 0000000000000000
//获取读锁的数量,即sharedCount(int c)方法,state无符号右移SHARED_SHIFT
0000000000000000 0000000000000010 //换算十进制等于2
2.1.2 写位运算
写位运算涉及的变量和方法如下:
//偏移位数
static final int SHARED_SHIFT = 16;
//获取低16位的条件
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
//获取写锁的数量
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
写锁使用低16位,所以写锁获取成功无需进行位移运算,只需+1就好。
要获取写锁的数量,使用的是 exclusiveCount() 方法,该方法把 state 和 EXCLUSIVE_MASK 做与运算。
EXCLUSIVE_MASK:1左移16位,然后后-1,得到65535,65535的二进制是111111111111111,然后高16位补0,即 EXCLUSIVE_MASK 为 00000000000000001111111111111111。
//接着上文读锁处,开始获取写锁时,state为
0000000000000010 0000000000000000
//来了一个线程获取写锁,state为 原值+1,结果为
0000000000000010 0000000000000001
//又来了1个线程获取写锁,state为
0000000000000010 0000000000000010
//获取写锁的重入数,即exclusiveCount(int c)方法,state&EXCLUSIVE_MASK
0000000000000000 0000000000000010 //换算十进制等于2
2.2 写锁详解——WriteLock
2.2.1 写锁的获取
示例伪代码如下:
ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
reentrantReadWriteLock.writeLock().lock();
reentrantReadWriteLock.writeLock().unlock();
写锁的获取,首先会调用 ReentrantReadWriteLock#writeLock() 方法返回一个写锁对象 WriteLock ,源码如下:
public class ReentrantReadWriteLock
implements ReadWriteLock, java.io.Serializable {
......
public ReentrantReadWriteLock.WriteLock writeLock() {
return writerLock;
}
......
}
接下来进行写锁加锁,即调用 WriteLock#lock() 方法,源码如下:
//ReentrantReadWriteLock#WriteLock
public void lock() {
sync.acquire(1); //独占
}
//AbstractQueuedSynchronizer
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
//ReentrantReadWriteLock#Sync
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
// 1. 获取写锁当前的同步状态
int c = getState();
// 2. 获取写锁获取的次数
int w = exclusiveCount(c);
if (c != 0) { //有读锁或者写锁
// (Note: if c != 0 and w == 0 then shared count != 0)
//写锁为0(证明有读锁),或者持有写锁的线程不为当前线程,获取写锁失败
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
//当前线程持有写锁,为重入锁,+acquires即可
setState(c + acquires);
return true;
}
//CAS操作失败,多线程情况下被抢占,获取锁失败。CAS成功则获取锁成功
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
流程如下:
- c != 0,说明有读锁或写锁;
- 通过 c != 0 和 w == 0 判断存在读锁(state不为0且写锁数为0,说明读锁数不为零)。
- 若有读锁,或者 写锁不为0且持有写锁的线程不为当前线程,获取写锁失败,返回fase。
- 若当前线程获取写锁的数量已经达到最大值,获取写锁失败,返回fase。
- 否则,当前线程获得锁。
- c==0,说明锁未被任何线程获取,直接CAS获取锁;失败返回fase
2.2.2 写锁的释放
//ReentrantReadWriteLock#WriteLock
public void unlock() {
sync.release(1);
}
//AbstractQueuedSynchronizer
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
//ReentrantReadWriteLock#Sync
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//1. 同步状态减去写状态
int nextc = getState() - releases;
//2. 当前写状态是否为0,为0则释放写锁
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
//3. 不为0则更新同步状态
setState(nextc);
return free;
}
- 这里需要注意的是,减去写状态 int nextc = getState() - releases;
- 因为的写状态是由同步状态的低16位表示的,所以只需要用当前同步状态直接减去写状态,不需要进行位移运算。
2.3 读锁详解
2.3.1 读锁的获取
源码如下:
//ReentrantReadWriteLock#WriteLock
public void lock() {
sync.acquireShared(1);
}
//AbstractQueuedSynchronizer
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
//ReentrantReadWriteLock#Sync
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
//1.如果写锁已经被获取并且获取写锁的线程不是当前线程的话,当前线程获取读锁失败返回-1
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
int r = sharedCount(c); //获取读锁的数量
if (!readerShouldBlock() && //读锁不需要阻塞
r < MAX_COUNT && //读锁小于最大读锁数量
compareAndSetState(c, c + SHARED_UNIT)) { //2.CAS操作尝试设置获取读锁 也就是高位加1
if (r == 0) { //若本次是第一个获取读锁
firstReader = current; //把第一次获取读锁的线程置为当前线程
firstReaderHoldCount = 1;
} else if (firstReader == current) { //若当前线程是第一个获取读锁的线程,现在再次获取
firstReaderHoldCount++;
} else { //若当前线程不是第一个获取读锁的线程,新当前线程的获取读锁数readHolds(ThreadLocal变量)
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;
}
//3.处理在第二步中CAS操作失败的,通过for循环自旋尝试获取读锁
return fullTryAcquireShared(current);
}
在上面的代码中尝试获取读锁的过程和获取写锁的过程也很相似,不同在于:
- 读锁只要没有写锁占用并且不超过最大获取数量都可以尝试获取读锁;
- 注意 compareAndSetState(c, c + SHARED_UNIT) ,这就是上文「读位运算」中所说的高位+1
- 写锁不仅需要考虑读锁是否占用,也要考虑写锁是否占用。
上面的代码中 firstReader,firstReaderHoldCount 以及 cachedHoldCounter 都是为 readHolds (ThreadLocalHoldCounter)服务的,用来记录每个读锁获取线程的获取次数,方便获取当前线程持有读锁的次数信息。在 ThreadLocal 基础上添加了一个 int 变量来统计次数,可以通过他们的实现来理解:
//ReentrantReadWriteLock#Sync
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> { //ThreadLocal变量
public HoldCounter initialValue() {
return new HoldCounter();
}
}
//ReentrantReadWriteLock#Sync
static final class HoldCounter {
int count = 0; //当前线程持有锁的次数
// Use id, not reference, to avoid garbage retention
final long tid = getThreadId(Thread.currentThread()); //当前线程ID
}
2.3.2 读锁的释放
源码如下:
//ReentrantReadWriteLock#WriteLock
public void unlock() {
sync.releaseShared(1);
}
//AbstractQueuedSynchronizer
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
//ReentrantReadWriteLock#Sync
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
// 前面还是为了实现getReadHoldCount等新功能
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(); // rh指向当前线程获取读锁的数量对象
int count = rh.count; // 当前线程获取读锁的数量
if (count <= 1) { // 若当前线程读锁数<=1,释放后就不再持有读锁了,此时调用ThreadLocal#remove()方法释放资源(详情见ThreadLocal一文)
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count; // 若当前线程读锁数>1,则数量-1
}
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT; // 读锁释放 将同步状态高位-1
if (compareAndSetState(c, nextc)) // 更新锁状态
// 若更新后的读写锁数量都为0,则返回true,这里是为了唤醒阻塞的writer线程
return nextc == 0;
}
}
2.4 锁降级
- 读写锁支持锁降级 :按照 获取写锁,获取读锁再释放写锁 的次序,写锁能够降级成为读锁。
- 不支持锁升级。
下面的伪代码显示了如何在更新缓存后执行锁降级,摘自 ReentrantWriteReadLock 源码中:
class CachedData {
Object data;
volatile boolean cacheValid;
final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
void processCachedData() {
rwl.readLock().lock();
if (!cacheValid) { //检查缓存状态
// 在获取写锁之前,必须先释放读锁
rwl.readLock().unlock();
rwl.writeLock().lock();
try {
//获取写锁后需要重新检查缓存状态,因为在我们之前,另一个线程可能已经获取了写锁并更改了缓存状态。
if (!cacheValid) {
data = ...
cacheValid = true;
}
// 锁降级:获取读锁后释放写锁
rwl.readLock().lock();
} finally {
rwl.writeLock().unlock(); // 释放了写锁,仍持有读锁
}
}
try {
use(data);
} finally {
rwl.readLock().unlock();
}
}
}