一、基本概念
在开发并发相关的项目时,经常涉及到对共享资源读和写的操作,且写操作没有读操作那么频繁。在没有写操作的时候,允许多个线程同时读取共享资源,并且不会造成线程安全问题。但是如果一个线程想去写这些共享资源,就必须阻塞其他线程对共享资源进行读和写的操作了。
针对以上场景,JDK源码包提供了读写锁工具类ReentrantReadWriteLock。
读锁可以多个线程同一时刻持有,写锁在同一时刻只能被一个线程持有。读锁和写锁共同使用才有意义,读锁可以使多线程访问数据时,如果仅仅是访问数据而不做修改,不用互相等待。但如果有线程要修改数据时,再加以限制,保证写数据的时候,别的线程不能再读取数据,以免读到的数据是污染数据。
在多线程操作共享资源时,读锁和写锁是如何互相影响的,下面以 A,B,C 3个线程做解释。
1、读读共享
A,B,C 三个线程可以同时持有读锁
2、读写互斥
当A,B同时持有读锁时,C想获取写锁,必须等A,B 的读锁全部释放。A,B 也必须等待读锁释放,才能再去获取写锁。锁重入不允许锁升级。
3、写读互斥
当A(有且仅有一个线程,写锁只能能同时被一个线程持有)持有写锁时,B,C想获取读锁,必须等A的写锁释放。但在A线程内,可以直接获取读锁,这是锁重入的概念。
4、写写互斥
当A(有且仅有一个线程,写锁只能能同时被一个线程持有)持有写锁时,B,C想获取写锁,必须等A的写锁释放。但在线程A内,可以直接获取写锁,这是锁重入的概念。
二、锁重入概念
NOTE : 锁重入是针对单线程内的一个概念
1、单线程内获取读锁后,读锁没释放之前,可以再次获取读锁
2、单线程内获取写锁后,写锁没释放之前,可以再次获取写锁
3、单线程内获取读锁后,读锁没释放之前,不能再获取写锁(锁升级)
4、单线程内获取写锁后,写锁没释放之前,可以再获取读锁(锁降级)
针对第1点和第2点分析:该特性基本不会显示的使用,在获取到读(写)锁后,再次去获取读(写)锁,本来就已经持有了,还去获取相同的锁,略显重复。
针对第3点分析:因为读锁是允许多个线程同时持有的,如果允许锁升级,那么这些持有读锁的线程就可以直接获取写锁,这样的结果是多个线程同时持有了写锁了,这与基本概念不符合,锁升级、锁降级是锁重入的概念。
针对第4点分析,在单个线程内(因为写锁同一时刻只可能被单个线程持有),持有了写锁,再去获取读锁,不违背同一时刻只有一个线程持有写锁这个基本概念。
三、使用场景
可以结合源码(ReentrantReadWriteLock.java)内的注释。
1、封装Collection和Map
原理:取数据是可以加读锁,修改数据时可以加写锁,这样可以保证多个线程可以同时读取集合内的 数据,但同时只允许一个线程修改集合数据。
源码注释为:
class RWDictionary {
private final Map<String, Data> m = new TreeMap<String, Data>();
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private final Lock r = rwl.readLock();
private final Lock w = rwl.writeLock();
public Data get(String key) {
r.lock();
try { return m.get(key); }
finally { r.unlock(); }
}
public String[] allKeys() {
r.lock();
try { return m.keySet().toArray(); }
finally { r.unlock(); }
}
public Data put(String key, Data value) {
w.lock();
try { return m.put(key, value); }
finally { w.unlock(); }
}
public void clear() {
w.lock();
try { m.clear(); }
finally { w.unlock(); }
}
}}
从RWDictionary类可以看出,对于读取数据的get和allKeys()方法只需加读锁,对于put和clear方法才加写锁,这样写数据时,避免脏数据的产生。
2、实现本地缓存
源码注释为:
class CachedData {
Object data;
volatile boolean cacheValid;//use volatile get data from Main-memoryfinal ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
void processCachedData() {
rwl.readLock().lock();
if (!cacheValid) {
// Must release read lock before acquiring write lock
rwl.readLock().unlock();
rwl.writeLock().lock();
try {
// Recheck state because another thread might have// acquired write lock and changed state before we did.if (!cacheValid) {
data = ...
cacheValid = true;
}
// Downgrade by acquiring read lock before releasing write lock
rwl.readLock().lock();
} finally {
rwl.writeLock().unlock(); // Unlock write, still hold read
}
}
try {
use(data);
} finally {
rwl.readLock().unlock();
}
}
}}
从CachedData代码可以看出,读锁和写锁共同 争抢的资源为 cacheValid这个布尔值。当要修改这个值时,需要在写锁内修改。
注意标红的注释,这几行注释描述得很详细。两个if(!cacheValid)跟单例模式里的DCL是一样的。假设线程A,第一次判断后,得到cacheValid=false,去获取写锁时,可能该写锁被别的线程争抢到了,并且将data和cacheValid值修改了。等别的线程将写锁释放后,线程A后续获得到了写锁,程序继续执行,需此时cacheValid的值已经变成true了,那么之前读到的cacheValid=false就是脏数据了,所以需要再次if(!cacheValid)。
CachedData 类的多个线程调用Demo:
CachedData cd = new CacheData();
new Thread(()->{cd.processCachedData()},"Thread-A").start();
new Thread(()->{cd.processCachedData()},"Thread-B").start();
new Thread(()->{cd.processCachedData()},"Thread-C").start();