你可能有这样一个疑问,Java SDK 并发包里为什么还有很多其他的工具类呢?原因很简单:分场景优化性能,提升易用性。
针对读多写少这种并发场景,Java SDK 并发包提供了读写锁——ReadWriteLock
读写锁,并不是 Java 语言特有的,而是一个广为使用的通用技术,所有的读写锁都遵守以下三条,尤其注意第三条,之后我们讲跟其他锁的对比会用到。
基本原则:
1. 允许多个线程同时读共享变量;
2.只允许一个线程写共享变量;
3. 如果一个写线程正在执行写操作,此时禁止读线程读共享变量(读锁 写锁是互斥的,不能同时存在)
互斥锁:
互斥锁 |
互斥锁 |
升降级 |
ReadWriteLock |
读操作允许多个线程同时读共享变量 写操作是互斥的,当一个 线程在写共享变量的时候,是不允许其他线程执行写操作和读操作。 |
不允许升级,允许降级。 读锁不支持条件变量newCondition() |
一:用 ReadWriteLock 快速实现一个通用的缓存工具类
1 class MyCache<K,V> { 2 3 final Map<K, V> m = new HashMap<>(); 4 final ReadWriteLock rwl = new ReentrantReadWriteLock(); 5 // 读锁 6 final Lock readLock = rwl.readLock(); 7 8 // 写锁 9 final Lock writeLock = rwl.writeLock(); 10 // 读缓存 11 V get(K key) { 12 readLock.lock(); 13 try { return m.get(key); } 14 finally { 15 readLock.unlock(); 16 } 17 } 18 19 // 写缓存 20 V put(String key, Data v) { 21 writeLock.lock(); 22 try { return m.put(key, v); } 23 finally { 24 writeLock.unlock(); 25 } 26 } 27 }
面的这段代码实现了按需加载的功能,这里我们假设缓存的源头是数据库。先从缓存中获取数据,若缓存中没有,就从数据库中加载,然后写入缓存。
经验之谈:在获取写锁之后,并不直接查数据库,而是再次验证缓存是否存在,为什么呢?
1 class MyCache<K,V> { 2 final Map<K, V> m = new HashMap<>(); 3 final ReadWriteLock rwl = new ReentrantReadWriteLock(); 4 final Lock r = rwl.readLock(); 5 final Lock w = rwl.writeLock(); 6 V get(K key) { 7 V v = null; 8 // 读缓存 9 r.lock(); ① 10 try { 11 v = m.get(key); ② 12 } finally{ 13 r.unlock(); ③ 14 } 15 // 缓存中存在,返回 16 if(v != null) { ④ 17 return v; 18 } 19 // 缓存中不存在,查询数据库 20 w.lock(); ⑤ 21 try { 22 // 获取到写锁,再次验证 23 v = m.get(key); ⑥ 24 if(v == null){ ⑦ 25 //假设A1线程获取读锁,并更新了数据库,释放了读锁 26 // 接下来A2线程得到读锁,获取读锁,并更新了数据库,释放了读锁 27 // A3线程也是如此操作, 28 // 获取到读锁再次验证是否存在,就能避免后续线程多次操作数据库 29 // 查询数据库 代码省略 30 //.................... 31 m.put(key, v); 32 } 33 } finally{ 34 w.unlock(); 35 } 36 return v; 37 } 38 }
二:锁的升级,在获取读锁后,发现是空的,(没有释放读锁),继续获得写锁,执行后续操作。实际上这种操作是不允许的,因为读锁和写锁互斥。
读锁还没有释放,此时获取写锁,会导致写锁永久等待,最终导致相关线程都被阻塞,永远也没有机会被唤醒。锁的升级是不允许的,这个你一定要注意。
// 读缓存 2
r.lock(); ① try { v = m.get(key); ② if (v == null) { w.lock(); try { // 再次验证并更新缓存 9 // 省略详细代码 } finally{ w.unlock(); } } } finally{ r.unlock(); ③ }
三:锁的降级
不过,虽然锁的升级是不允许的,但是锁的降级却是允许的。以下代码来源自ReentrantReadWriteLock 的官方示例,伪代码如下
1 class CachedData { 2 Object data; 3 volatile boolean cacheValid; 4 final ReadWriteLock rwl = new ReentrantReadWriteLock(); 5 // 读锁 6 final Lock r = rwl.readLock(); 7 // 写锁 8 final Lock w = rwl.writeLock(); 9 10 void processCachedData() { 11 // 获取读锁 12 r.lock(); 13 if (!cacheValid) { 14 // 释放读锁,因为不允许读锁的升级 15 r.unlock(); 16 // 获取写锁 17 w.lock(); 18 try { 19 // 再次检查状态 20 if (!cacheValid) { 21 data = ... 22 cacheValid = true; 23 } 24 // 释放写锁前,降级为读锁 25 // 降级是可以的 26 r.lock(); ① 27 } finally { 28 // 释放写锁 29 w.unlock(); 30 } 31 } 32 // 此处仍然持有读锁 33 try {use(data);} 34 finally {r.unlock();} 35 } 36 }
总结:
读写锁类似于 ReentrantLock,也支持公平模式和非公平模式。读锁和写锁都实现了java.util.concurrent.locks.Lock 接口,所以除了支持 lock() 方法外,tryLock()、lockInterruptibly() 等方法也都是支持的。但是有一点需要注意,那就是只有写锁支持条件变量,读锁是不支持条件变量的,读锁调用 newCondition() 会抛出 UnsupportedOperationException异常
文中部分代码引用:王宝令 ReadWriteLock:如何快速实现一个完备的缓存