并发库应用之五 & ReadWriteLock场景应用
锁降级:从写锁变成读锁;
锁升级:从读锁变成写锁。
读锁是可以被多线程共享的,写锁是单线程独占的。也就是说写锁的并发限制比读锁高,这可能就是升级/降级名称的来源。
如下代码会产生死锁,因为同一个线程中,在没有释放读锁的情况下,就去申请写锁,这属于锁升级,ReentrantReadWriteLock是不支持的。
ReadWriteLock rtLock = new ReentrantReadWriteLock(); rtLock.readLock().lock(); System.out.println("get readLock."); rtLock.writeLock().lock(); System.out.println("blocking");
ReentrantReadWriteLock支持锁降级,如下代码不会产生死锁。
ReadWriteLock rtLock = new ReentrantReadWriteLock(); rtLock.writeLock().lock(); System.out.println("writeLock"); rtLock.readLock().lock(); System.out.println("get read lock");
以上这段代码虽然不会导致死锁,但没有正确的释放锁。从写锁降级成读锁,并不会自动释放当前线程获取的写锁,仍然需要显示的释放,否则别的线程永远也获取不到写锁。
1 class CachedData { 2 Object data; 3 volatile boolean cacheValid; 4 final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); 5 6 public void processCachedData() { 7 rwl.readLock().lock(); 8 if (!cacheValid) { 9 // Must release read lock before acquiring write lock 10 rwl.readLock().unlock(); 11 rwl.writeLock().lock(); 12 try { 13 // Recheck state because another thread might have,acquired write lock and changed state before we did. 14 if (!cacheValid) { 15 data = ... 16 cacheValid = true; 17 } 18 // 在释放写锁之前通过获取读锁降级写锁(注意此时还没有释放写锁) 19 rwl.readLock().lock(); 20 } finally { 21 rwl.writeLock().unlock(); // 释放写锁而此时已经持有读锁 22 } 23 } 24 25 try { 26 use(data); 27 } finally { 28 rwl.readLock().unlock(); 29 } 30 } 31 }
以上代码加锁的顺序为:
1. rwl.readLock().lock();
2. rwl.readLock().unlock();
3. rwl.writeLock().lock();
4. rwl.readLock().lock();
5. rwl.writeLock().unlock();
6. rwl.readLock().unlock();
以上过程整体讲解:
1. 多个线程同时访问该缓存对象时,都加上当前对象的读锁,之后其中某个线程优先查看data数据是否为空。【加锁顺序序号:1 】
2. 当前查看的线程发现没有值则释放读锁立即加上写锁,准备写入缓存数据。(不明白为什么释放读锁的话可以查看上面讲解进入写锁的前提条件)【加锁顺序序号:2和3 】
3. 为什么还会再次判断是否为空值(!cacheValid)是因为第二个、第三个线程获得读的权利时也是需要判断是否为空,否则会重复写入数据。
4. 写入数据后先进行读锁的降级后再释放写锁。【加锁顺序序号:4和5 】
5. 最后数据数据返回前释放最终的读锁。【加锁顺序序号:6 】
如果不使用锁降级功能,如先释放写锁,然后获得读锁,在这个get过程中,可能会有其他线程竞争到写锁 或者是更新数据 则获得的数据是其他线程更新的数据,可能会造成数据的污染,即产生脏读的问题。
下面,让我们来实现真正趋于实际生产环境中的缓存案例:
1 import java.util.HashMap; 2 import java.util.Map; 3 import java.util.concurrent.locks.ReadWriteLock; 4 import java.util.concurrent.locks.ReentrantReadWriteLock; 5 6 public class CacheDemo { 7 /** 8 * 缓存器,这里假设需要存储1000左右个缓存对象,按照默认的负载因子0.75,则容量=750,大概估计每一个节点链表长度为5个 9 * 那么数组长度大概为:150,又有雨设置map大小一般为2的指数,则最近的数字为:128 10 */ 11 private Map<String, Object> map = new HashMap<>(128); 12 private ReadWriteLock rwl = new ReentrantReadWriteLock(); 13 public static void main(String[] args) { 14 15 } 16 public Object get(String id){ 17 Object value = null; 18 rwl.readLock().lock();//首先开启读锁,从缓存中去取 19 try{
if(map.get(id) == null){ //如果缓存中没有释放读锁,上写锁 22 rwl.readLock().unlock(); 23 rwl.writeLock().lock(); 24 try{ 25 if(value == null){ //防止多写线程重复查询赋值 26 value = "redis-value"; //此时可以去数据库中查找,这里简单的模拟一下 27 } 28 rwl.readLock().lock(); //加读锁降级写锁,不明白的可以查看上面锁降级的原理与保持读取数据原子性的讲解 29 }finally{ 30 rwl.writeLock().unlock(); //释放写锁 31 } 32 } 33 }finally{ 34 rwl.readLock().unlock(); //最后释放读锁 35 } 36 return value; 37 } 38 }
提示:读写锁之后有一个与它配合使用的有条件的阻塞,可以实现线程间的通信,它就是Condition。具体详情请查看我的博客:并发库应用之六 & 有条件阻塞Condition应用