ReentrantReadWriteLock 读写锁的使用及原理
一. 介绍
在之前讲到的 synchronized 和 ReentrantLock 都是排他锁,也就是同一时刻只能有一个线程获取到锁。而读写锁在同一时刻允许多个线程获取锁,读写锁顾名思义,有读锁和写锁,在读锁的时候,允许多个线程同时获取,但是在一个线程获取写锁的时候,其他获取到读锁或者写锁的线程都会阻塞。
二. 读写锁实现缓存
读写锁最常用的例子就是用锁的性质来实现一个缓存,缓存的特点就是读多写少,如果使用普通的锁来实现,每次都只能有一个线程获取锁,其他的线程都会阻塞。而读写锁就能给多个进行读操作的线程进行锁的并发。
在缓存的例子中,使用了一个非线程安全的 HashMap,但是在实际获取值的时候使用了读锁和写锁保证线程安全。
public class Cache {
private static final Map<String, Object> map;
private static final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private static final Lock readLock = readWriteLock.readLock();
private static final Lock writeLock = readWriteLock.writeLock();
static {
map = new HashMap<>();
}
public static Object get(String key) {
readLock.lock();
try {
return map.get(key);
} finally {
readLock.unlock();
}
}
public static Object put(String key, Object value) {
writeLock.lock();
try {
return map.put(key, value);
} finally {
writeLock.unlock();
}
}
}
三. 读锁和写锁的实现
读写锁的实现也是依靠对队列同步器的自定义实现。同时还和 ReentrantLock 一样分为公平锁和非公平锁,在尝试获取锁的时候,也会根据是否是公平锁来判断是否要在等待队列中等待。
1. 读锁
读锁是一个支持多个线程获取的锁,但是读锁是和写锁排斥的,读锁实现了队列同步器中的共享式获取同步状态的方法。因为有可能会有多个线程获取锁,所以在获取锁方法的实现上,使用了 CAS 以及循环多次尝试来成功获取锁。
public static class ReadLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = -5992448646407690164L;
private final Sync sync;
protected ReadLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
public void lock() {
sync.acquireShared(1);
}
public void lockInterruptibly() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public boolean tryLock() {
return sync.tryReadLock();
}
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
public void unlock() {
sync.releaseShared(1);
}
}
锁的获取
因为读锁可以有多个线程获取,所以在获取锁的时候会计算读锁的数量,并且读锁有最大数量的限制为 65535。如果在获取读锁的时候碰到了有线程获取了写锁,那读锁会获取失败。如果在 tryAcquireShared() 中获取锁的时候失败了,还会在最后通过 fullTryAcquireShared() 方法不断循环再次尝试获取,直到获取完成。
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);
}
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {
int c = getState();
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
} else if (readerShouldBlock()) {
if (firstReader == current) {
} else {
if (rh == null) {
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) {
rh = readHolds.get();
if (rh.count == 0)
readHolds.remove();
}
}
if (rh.count == 0)
return -1;
}
}
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
if (compareAndSetState(c, c + SHARED_UNIT)) {
if (sharedCount(c) == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
if (rh == null)
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
cachedHoldCounter = rh;
}
return 1;
}
}
}
锁的释放
读锁因为有可能会被多个线程持有,所以锁的释放会释放多个读锁,直到所有的读锁都被释放才算结束。
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
if (firstReader == current) {
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))
return nextc == 0;
}
}
2. 写锁
写锁主要实现的独占式获取同步状态的方法,同时只能有一个线程获取写锁。
public static class WriteLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = -4992448646407690164L;
private final Sync sync;
protected WriteLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
public void lock() {
sync.acquire(1);
}
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
public boolean tryLock( ) {
return sync.tryWriteLock();
}
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
public void unlock() {
sync.release(1);
}
}
锁的获取
在锁的获取最后一步 writerShouldBlock() 还会根据是否是公平锁来判断当前队列中线程的节点是否是等待时间最长的,如果了解队列同步器,就知道,里面维护了一套 FIFO 的队列,如果是公平型的写锁的获取,并且队列中还有其他先进入的线程在等待,那只能先等其他线程获取写锁。
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;
}
锁的释放
写锁的释放每次都会减少写状态,如果写状态减为 0,则代表写锁已经释放。
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;
}
四. 锁降级
锁降级指的是写锁降级为读锁,其中先持有写锁,然后在不释放写锁的情况下再获取读锁,获取到读锁以后再释放写锁,这种才是真正的降级,而不是先释放写锁再获取读锁。在这里面,锁是不允许升级的,如果进行锁的升级,有可能导致获取写锁的线程被永久阻塞。
在源码中给了一套锁降级的例子。
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();
}
}
}
为什么不能升级锁
在写锁的获取锁的源码中,会先判断 state 变量是否有值,也就是是否有其他线程获取了锁。如果有线程已经获取到读锁了,但是在获取写锁之前没有释放读锁,这时候这个 state 变量不为 0。
但是因为读锁在获取锁的时候没有设置当前锁定的线程,也就是 getExclusiveOwnerThread() 会获取到 null 值,这个时候会判定为写锁获取失败并加入到队列中。但是在 AQS 的源码中知道,获取独占式锁的过程只有构造节点和自旋并阻塞线程的过程,并没有通知其他等待队列中的节点的过程,这个过程只发生在 release() 方法中,那这时候就会发生一个线程永远阻塞了,其他等待中的节点也没办法通知到,整个过程就会被阻塞。
相反,如果锁降级了,在持有写锁的时候再去获取死锁,就没有这些判断。
- tryAcquire():写锁获取过程
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
// 如果这边没有事先释放读锁,值就不是 0
int c = getState();
int w = exclusiveCount(c);
if (c != 0) {
// 有其他锁并且写锁的数量是 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;
}
本文介绍了Java的ReentrantReadWriteLock,一种允许多个读取者或单一写入者同时访问资源的锁机制。文章详细阐述了读写锁如何用于实现缓存,以及读锁和写锁的获取与释放过程,强调了锁降级的重要性及其原理,同时解释了为何不能升级锁的原因。
5710

被折叠的 条评论
为什么被折叠?



