一、简介
读写锁在同一时刻允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了两把锁,一把读锁和一把写锁。获取读写锁可分为下面两种情况:
- 同一线程:该线程获取读锁后,能够再次获取读锁,但不能获取写锁。该线程获取写锁后,能够再次获取写锁,也可以再获取读锁。
- 不同线程: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的实现,同步状态表示锁被获取的次数,而读写锁的自定义同步器需要在同步状态(一个整型变量)上维护多个读线程和一个写线程的状态,该状态的设计成为设计读写锁的关键。
如果在一个整型变量上维护多种状态,就需要“按位切割使用”这个变量,读写锁将变量切分成了两部分,高16位表示读,低16位表示写,划分方式如下图。
上图同步状态表示一个线程已经获取了写锁,且重进入了两次,同时也连续获取了两次读锁。读写锁时如何确定读和写的状态?答案是通过位运算。假设当前同步状态为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,因为大多数场景读是远大于写的,所以在读多于写的情况下,读写锁能够提供更好的并发性和吞吐量。