「java.util.concurrent并发包8」之 ReentrantReadWriteLock
一 概述
多线程的环境下对同一份数据进行读写,会涉及到线程安全的问题。比如一个线程读取数据的时候,另外一个线程在写数据,会导致前后数据的不一致。一个线程在写数据,另一个线程也在写,同样会导致线程前后看到的数据不一致。这时可以在读写方法加入互斥锁,任何时候只能允许一个线程的一个读或写操作,而不允许其他线程的读或写操作,这样是可以解决这样以上的问题,但是效率却大打折扣了。因为在真实的业务场景中,一份数据,读取数据的操作次数通常高于写入数据的操作,而线程与线程间的读读操作是不涉及到线程安全的问题,没有必要加入互斥锁,只要在读-写,写-写期间上锁就行了
读写锁:分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由jvm自己控制的。
ReentrantReadWriteLock是可重入的读写锁,允许多个读线程获得ReadLock,但只允许一个写线程获得WriteLock
二 锁升级锁降级
读写锁的机制:
ReadWriteLock rtLock = new ReentrantReadWriteLock(); rtLock.readLock().lock(); System.out.println("get readLock."); rtLock.writeLock().lock(); System.out.println("blocking");
这个代码会死锁,没释放读锁就去申请写锁
ReadWriteLock rtLock = new ReentrantReadWriteLock(); rtLock.writeLock().lock(); System.out.println("writeLock"); rtLock.readLock().lock(); System.out.println("get read lock");
以上这段代码虽然不会导致死锁,但没有正确的释放锁。从写锁降级成读锁,并不会自动释放当前线程获取的写锁,仍然需要显示的释放,否则别的线程永远也获取不到写锁。
关于读写锁里面有一个锁升级和降级的问题,也就是写锁可以降级为读锁,但是读锁却不能升级为写锁。那么为什么是这样?
其实也不难理解,只要线程获取写锁,那么这一刻只有这一个线程可以在临界区操作,它自己写完的东西,自己的是可以看见的,所以写锁降级为读锁是非常自然的一种行为,并且几乎没有任何性能影响,但是反过来就不一定行的通了,因为读锁是共享的,也就是说同一时刻有大量的读线程都在临界区读取资源,如果可以允许读锁升级为写锁,这里面就涉及一个很大的竞争问题,所有的读锁都会去竞争写锁,这样以来必然引起巨大的抢占,这是非常复杂的,因为如果竞争写锁失败,那么这些线程该如何处理?是继续还原成读锁状态,还是升级为竞争写锁状态?这一点是不好处理的,所以Java的api为了让语义更加清晰,所以只支持写锁降级为读锁,不支持读锁升级为写锁。
举个生活中的例子,在一个演唱会中,台上有一名歌手在唱歌,我们可以理解为它是写锁,只有他在唱歌,同时台下有很多观众在听歌,观众也就是读锁,现在假如歌手唱完了,它可以立马到台下很轻松的就降级为一名观众,但是反过来我们宣布一项规定,谁先登上舞台上,谁就是歌手可以演唱一首歌并获得奖金,如果真的是这样,那么所有人必然会蜂拥而上,这时候就乱了,弄不好还会出现踩踏事故,所以观众升级为歌手这件事情代价是比较大的。
这就是读锁为什么不能直接升级写锁的主要原因,当然这里并不是绝对,升级写锁的最佳条件是一次只允许一个读线程升级,这样以来就不会产生大量不可控的竞争,在JDK8中新增的StampedLock类就可以比较优雅的完成这件事,这个到后面我们再分析。
三 读写锁实现缓存
1 private static Map<Integer, Integer> cache = Maps.newHashMap(); 2 private ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); 3 4 public Integer get(Integer key) { 5 Integer value; 6 readWriteLock.readLock().lock(); 7 try { 8 value = cache.get(key); 9 if (value == null) { 10 readWriteLock.readLock().unlock(); 11 readWriteLock.writeLock().lock(); 12 try { 13 if (value == null) { 14 value = 1; // 从数据库读取等 15 } 16 readWriteLock.readLock().lock(); 17 } finally { 18 readWriteLock.writeLock().unlock(); 19 } 20 } 21 } finally { 22 readWriteLock.readLock().unlock(); 23 } 24 return value; 25 } 26 27 public void put(Integer key, Integer value) { 28 readWriteLock.writeLock().lock(); 29 cache.put(key, value); 30 readWriteLock.writeLock().unlock(); 31 }
注意最后的释放写锁「line18」,在之前是要加读锁「line16」的,因为在get过程中,可能有其他线程竞争到锁或是更新数据,会产生脏读。
(延伸:释放写锁前先加上读锁~)