ReentrantReadWriteLock是一个读写锁,它内部维护了两个锁:ReadLock 和WriteLock。ReadLock用于只读操作,WriteLock用于写操作。 如果没有写操作,读锁是可以被多个线程同时持有的,即写锁是独占的,读锁是共享的。
java
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReentrantReadWriteLockExample {
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
public void read() {
rwLock.readLock().lock();
try {
System.out.println("Read lock acquired by thread " + Thread.currentThread().getName());
// 读取操作
} finally {
rwLock.readLock().unlock();
}
}
public void write() {
rwLock.writeLock().lock();
try {
System.out.println("Write lock acquired by thread " + Thread.currentThread().getName());
// 写入操作
} finally {
rwLock.writeLock().unlock();
}
}
}
上面的这个例子中,read() 和 write() 方法都用相应的锁来保护。这样可以保证同时只有一个线程可以写入,同时可以有多个线程读取。
注意:
- 读锁不支持条件变量
- 重入不支持升级:即当某个线层持有了读锁,再重入去获取写锁,会导致永久等待
- 重入支持降级:即当某个线程持有写锁,再重入去获取读锁,是可以获取读锁的
读写锁实现原理
读读的情况下共享,因为没有对资源进行任何操作,其实是不会存在线程安全问题的,而写写互斥,因为写锁是独占锁,只会有一个线程会持有写锁,即独占锁资源,与ReentrantLock是差不多的。当读写的情况下,是通过状态来进行互斥的。
ReentrantReadWriteLock也是基于AQS来实现的,但是与ReentrantLock不同的是,ReentrantReadWriteLock内部有两个锁,这就意味着锁状态也是需要两个去控制的,而AQS提供了一个state进行控制,最终ReentrantReadWriteLock 使用了 AQS 的同步状态(state)的高 16 位来表示读锁的持有数量,低 16 位表示写锁的重入次数。这样就可以在一个 int 变量中同时表示读锁和写锁的状态。
原理
读锁获取
当一个线程尝试获取读锁时,首先会检查写锁状态(即 state 的低 16 位),如果写锁未被占用(即写锁状态为 0),那么读锁状态就会增加(即 state 的高 16 位加 1),然后该线程就获取到了读锁。如果写锁已经被占用,那么如果当前持有写锁的线程就是请求读锁的线程,那么允许该线程获取读锁(即写锁可以降级为读锁),否则请求读锁的线程就会被阻塞,直到写锁被释放。
写锁获取
当一个线程尝试获取写锁时,首先会检查 state 是否为 0,如果是,那么 state 就会增加 1(即写锁状态增加 1),然后该线程就获取到了写锁。如果 state 不为 0,那么如果当前持有锁的线程就是请求写锁的线程,那么 state 就会增加 1(即写锁的重入次数增加 1),否则请求写锁的线程就会被阻塞,直到锁被完全释放。
读锁释放
当一个线程释放读锁时,读锁状态就会减少(即 state 的高 16 位减 1)。如果读锁状态变为 0,那么等待的写线程就有可能获取到写锁。
写锁释放
当一个线程释放写锁时,写锁状态就会减少(即 state 的低 16 位减 1)。如果写锁状态变为 0,那么等待的读线程和写线程都有可能获取到锁。
锁降级
锁降级指的是写锁降级成为读锁。如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级。锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。 锁降级可以帮助我们拿到当前线程修改后的结果而不被其他线程所破坏,防止更新丢失。
java
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class LockDowngradeExample {
private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private int data = 0;
public void lockDowngrade() {
// 首先获取写锁
rwLock.writeLock().lock();
try {
data = data + 1;
System.out.println("Data updated to: " + data);
// 然后在持有写锁的情况下获取读锁,这是安全的
rwLock.readLock().lock();
} finally {
// 最后释放写锁,此时已经持有读锁,所以这是一个锁降级的过程
rwLock.writeLock().unlock();
}
try {
// 这个时候已经降级为读锁,可以安全地读取数据
System.out.println("Reading data: " + data);
} finally {
// 最后释放读锁
rwLock.readLock().unlock();
}
}
public static void main(String[] args) {
LockDowngradeExample example = new LockDowngradeExample();
example.lockDowngrade();
}
}
上面例子实现了一个锁降级过程,首先获取写锁对数据进行更新,然后在持有写锁的情况下获取读锁,最后释放写锁进行锁降级。在这个过程中,数据始终被保护,不会被其他线程干扰。
锁升级
ReentrantReadWriteLock并不支持锁的升级,这是因为锁升级可能导致死锁。如果一个线程已经持有了读锁,然后尝试获取写锁,那么这个线程将会被永远阻塞,因为写锁只能在没有线程持有读锁时才能被获取,而这个线程自己就持有一个读锁,除非它释放读锁,否则写锁将永远无法被获取,这就形成了一个死锁。
java
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class LockUpgradeExample {
private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
public void lockUpgrade() {
// 首先获取读锁
rwLock.readLock().lock();
try {
System.out.println("Acquired read lock, attempting to acquire write lock...");
// 然后尝试获取写锁,这将导致死锁
rwLock.writeLock().lock();
System.out.println("Lock upgraded, this will not print!");
} finally {
rwLock.readLock().unlock();
rwLock.writeLock().unlock();
}
}
public static void main(String[] args) {
LockUpgradeExample example = new LockUpgradeExample();
example.lockUpgrade();
}
}
上述代码实现了一个锁升级示例,在这段代码中,线程首先获取了读锁,然后尝试获取写锁。然而,由于 ReentrantReadWriteLock 不支持锁升级,尝试获取写锁的操作将永远阻塞,导致死锁。
实现流程
java
// 写锁加锁逻辑
protected final boolean tryAcquire(int acquires) {
/*
* Walkthrough:
* 1. If read count nonzero or write count nonzero
* and owner is a different thread, fail.
* 2. If count would saturate, fail. (This can only
* happen if count is already nonzero.)
* 3. Otherwise, this thread is eligible for lock if
* it is either a reentrant acquire or
* queue policy allows it. If so, update state
* and set owner.
*/
// 获取当前新线程
Thread current = Thread.currentThread();
// 获取AQS的state状态
int c = getState();
// 获取state状态低16位(写锁状态)
int w = exclusiveCount(c);
if (c != 0) { // 读写锁有值
// (Note: if c != 0 and w == 0 then shared count != 0)
if (w == 0 || current != getExclusiveOwnerThread()) // 写锁里没有值或者当前线程不等于持有锁的线程,加锁失败
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT) throw new Error("Maximum lock count exceeded");
// Reentrant acquire
// 重入
setState(c + acquires);
return true;
}
// 尝试修改state状态
if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) return false;
setExclusiveOwnerThread(current);
return true;
}
java
// 读锁加锁逻辑
protected final int tryAcquireShared(int unused) {
/*
* Walkthrough:
* 1. If write lock held by another thread, fail.
* 2. Otherwise, this thread is eligible for
* lock wrt state, so ask if it should block
* because of queue policy. If not, try
* to grant by CASing state and updating count.
* Note that step does not check for reentrant
* acquires, which is postponed to full version
* to avoid having to check hold count in
* the more typical non-reentrant case.
* 3. If step 2 fails either because thread
* apparently not eligible or CAS fails or count
* saturated, chain to version with full retry loop.
*/
// 获取当前线程
Thread current = Thread.currentThread();
// 获取AQS的state状态
int c = getState();
// 低16位(写锁状态)不等于0,表示存在写锁,并且当前线程不是持有写锁的线程,加锁失败
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
// 获取高16位(读锁)状态
int r = sharedCount(c);
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) { // 还没有线程持有读锁
// 设置首个持有读锁的线程
firstReader = current;
// 设置持有读锁的个数,重入时+1
firstReaderHoldCount = 1;
} else if (firstReader == current) { // 已经有线程持有读锁,并且是第一个持有读锁的线程,则状态+1,可重入实现
firstReaderHoldCount++;
} else { // 已经有线程持有读锁,并且不是第一个持有读锁的线程
// 创建HoldCounter来进行读锁可重入计数
HoldCounter rh = cachedHoldCounter;
// 创建ThreadLocal来存可重入计数
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 是非公平的,这意味着在高并发的情况下,读锁可能会一直占用,导致写锁长时间无法获取,也就是发生了写锁饥饿。虽然可以通过构造函数指定为公平锁来避免这个问题,但是公平锁的性能会稍低一些。
jdk1.8引入了StampedLock,这是一个新的读写锁,设计目标就是为了解决ReentrantReadWriteLock在高并发场景下的一些性能问题。
使用场景
- 读多写少
- 需要读写分离
- 缓存