一、读锁和写锁介绍
读锁(共享锁):可多条线程同时获取数据,多个线程可以同时占有
写锁(独占锁):只能单条线程写入,一次只能被一个线程占有
读锁和写锁是互斥的,加了读锁后,不能再加写锁,保证了读操作的数据原子性;同理,加了写锁不能再加读锁。
现实中有这样一种场景:对共享资源有读和写的操作,且写操作没有读操作那么频繁。在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源;但是如果一个线程想去写这些共享资源,就不应该允许其他线程对该资源进行读和写的操作了。
针对这种场景,JAVA的并发包提供了读写锁ReentrantReadWriteLock,它表示两个锁,一个是读操作相关的锁,称为共享锁;一个是写相关的锁,称为排他锁,描述如下:
线程进入读锁的前提条件:
- 没有其他线程的写锁,
- 没有写请求或者有写请求,但调用线程和持有锁的线程是同一个。
线程进入写锁的前提条件:
- 没有其他线程的读锁
- 没有其他线程的写锁
ReentrantReadWriteLock支持以下功能:
1)支持公平和非公平的获取锁的方式;
2)支持可重入。读线程在获取了读锁后还可以获取读锁;写线程在获取了写锁之后既可以再次获取写锁又可以获取读锁;
3)还允许从写入锁降级为读取锁,其实现方式是:先获取写入锁,然后获取读取锁,最后释放写入锁。但是,从读取锁升级到写入锁是不允许的;
4)读取锁和写入锁都支持锁获取期间的中断;
5)Condition支持。仅写入锁提供了一个 Conditon 实现;读取锁不支持 Conditon ,readLock().newCondition() 会抛出 UnsupportedOperationException。
二、ReentrantReadWriteLock使用介绍
2.1 利用重入来执行升级缓存后的锁降级
class CachedData {
Object data;
volatile boolean cacheValid; //缓存是否有效
ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
void processCachedData() {
rwl.readLock().lock(); //获取读锁
//如果缓存无效,更新cache;否则直接使用data
if (!cacheValid) {
// Must release read lock before acquiring write lock
//获取写锁前须释放读锁
rwl.readLock().unlock();
rwl.writeLock().lock();
// 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();
rwl.writeLock().unlock(); // Unlock write, still hold read
}
use(data);
rwl.readLock().unlock(); //释放读锁
}
}
2.2 ReentrantReadWriteLock 提高并发性
通常在 collection 数据很多,读线程访问多于写线程并且 entail 操作的开销高于同步开销时尝试这么做。
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(); }
}
}
我们举个例子来比较ReentrantLock和ReentrantReadWriteLock的并发性能。
ReentrantLock例子:
class RWDictionary2 {
private final Map<String, String> m = new TreeMap<String, String>();
private final Lock lock = new ReentrantLock();
public String get(String key) {
lock.lock();
try {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return m.get(key);
}
finally { lock.unlock(); }
}
public int keySize() {
lock.lock();
try {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return m.size();
}
finally { lock.unlock(); }
}
public String put(String key, String value) {
lock.lock();
try {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return m.put(key, value);
}
finally { lock.unlock(); }
}
public static void main(String[] args) {
RWDictionary2 dictionary2 = new RWDictionary2();
new Thread(() -> {
for (int i = 1; i <= 50; i++) {
dictionary2.put(i + "", "put");
System.out.println("put 用时:"+System.currentTimeMillis());
}
}, "put").start();
new Thread(() -> {
for (int i = 1; i <= 50; i++) {
dictionary2.get(i + "");
System.out.println("get 用时:"+System.currentTimeMillis());
}
}, "get").start();
new Thread(() -> {
for (int i = 1; i <= 50; i++) {
dictionary2.keySize();
System.out.println("keySize 用时:"+System.currentTimeMillis());
}
}, "allKeys").start();
}
}
执行结果:
put 用时:1649216011387
get 用时:1649216014390
keySize 用时:1649216017401
put 用时:1649216020404
get 用时:1649216023414
keySize 用时:1649216026423
put 用时:1649216029425
get 用时:1649216032435
keySize 用时:1649216035449
解读代码:
上述代码,定义了两个读方法和一个写方法,而且三个方法都共用了一个ReentrantLock锁。因此结果也就可想而知,三个方法都是同步的,间隔三秒,并发性能严重受到影响。(此案例会出现数据错误,比如先执行了读再执行写,这里忽略,不关注数据正确性)
不给读方法加锁?不行,读方法不加锁,就随时可以执行,会出现脏读数据错误,甚至并发异常。
我们的目标是,保证写的时候不允许读,读的时候不允许写,可以多个线程一起读,仅能一个线程单独写。
ReentrantReadWriteLock 例子:
class RWDictionary {
private final Map<String, String> m = new TreeMap<String, String>();
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private final Lock r = rwl.readLock(); //读锁
private final Lock w = rwl.writeLock(); //写锁
public String get(String key) {
r.lock();
try {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return m.get(key);
}
finally { r.unlock(); }
}
public int keySize() {
r.lock();
try {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return m.size();
}
finally { r.unlock(); }
}
public String put(String key, String value) {
w.lock();
try {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return m.put(key, value);
}
finally { w.unlock(); }
}
public void clear() {
w.lock();
try {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
m.clear();
}
finally { w.unlock(); }
}
public static void main(String[] args) {
RWDictionary dictionary = new RWDictionary();
new Thread(() -> {
for (int i = 1; i <= 50; i++) {
dictionary.put(i + "", "put");
System.out.println("put 用时:"+System.currentTimeMillis());
}
}, "put").start();
new Thread(() -> {
for (int i = 1; i <= 50; i++) {
dictionary.put(i + "", "clear");
System.out.println("clear 用时:"+System.currentTimeMillis());
}
}, "clear").start();
new Thread(() -> {
for (int i = 1; i <= 50; i++) {
dictionary.get(i + "");
System.out.println("get 用时:"+System.currentTimeMillis());
}
}, "get").start();
new Thread(() -> {
for (int i = 1; i <= 50; i++) {
dictionary.keySize();
System.out.println("keySize 用时:"+System.currentTimeMillis());
}
}, "allKeys").start();
}
}
执行结果:
put 用时:1649216720385
clear 用时:1649216723395
get 用时:1649216726402
keySize 用时:1649216726402
put 用时:1649216729415
clear 用时:1649216732427
keySize 用时:1649216735429
get 用时:1649216735429
put 用时:1649216738443
clear 用时:1649216741445
keySize 用时:1649216744452
get 用时:1649216744452
解读代码:
上述代码有两个写方法,两个读方法,两个写方法都只能单独执行并且间隔三秒,两个读方法可以并发执行间隔时间可忽略。如此就提高了读的并发性能。
三、ReentrantReadWriteLock 实现原理
暂时省略,后补
四、文章参考来源
https://www.cnblogs.com/zaizhoumo/p/7782941.html
https://www.cnblogs.com/xiaoxi/p/9140541.html