本人免费整理了Java高级资料,涵盖了Java、Redis、MongoDB、MySQL、Zookeeper、Spring Cloud、Dubbo高并发分布式等教程,一共30G,须要本身领取。
传送门:https://mp.weixin.qq.com/s/JzddfH-7yNudmkjT0IRL8Q
Lock比传统线程模型中的synchronized方式更加面向对象,与生活中的锁相似,锁自己也应该是一个对象。两个线程执行的代码片断要实现同步互斥的效果,它们必须用同一个Lock对象。java
读写锁:分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由jvm本身控制的,咱们只要上好相应的锁便可。若是你的代码只读数据,能够不少人同时读,但不能同时写,那就上读锁;若是你的代码修改数据,只能有一我的在写,且不能同时读取,那就上写锁。总之,读的时候上读锁,写的时候上写锁!redis
读写锁接口:ReadWriteLock,它的具体实现类为:ReentrantReadWriteLock数据库
在多线程的环境下,对同一份数据进行读写,会涉及到线程安全的问题。好比在一个线程读取数据的时候,另一个线程在写数据,而致使先后数据的不一致性;一个线程在写数据的时候,另外一个线程也在写,一样也会致使线程先后看到的数据的不一致性。数组
这时候能够在读写方法中加入互斥锁,任什么时候候只能容许一个线程的一个读或写操做,而不容许其余线程的读或写操做,这样是能够解决这样以上的问题,可是效率却大打折扣了。由于在真实的业务场景中,一份数据,读取数据的操做次数一般高于写入数据的操做,而线程与线程间的读读操做是不涉及到线程安全的问题,没有必要加入互斥锁,只要在读-写,写-写期间上锁就好了。缓存
对于以上这种状况,读写锁是最好的解决方案!其中它的实现类:ReentrantReadWriteLock--顾名思义是可重入的读写锁,容许多个读线程得到ReadLock,但只容许一个写线程得到WriteLock安全
读写锁的机制:多线程
"读-读" 不互斥并发
"读-写" 互斥jvm
"写-写" 互斥分布式
ReentrantReadWriteLock会使用两把锁来解决问题,一个读锁,一个写锁。
线程进入读锁的前提条件:
1. 没有其余线程的写锁
2. 没有写请求,或者有写请求但调用线程和持有锁的线程是同一个线程
进入写锁的前提条件:
1. 没有其余线程的读锁
2. 没有其余线程的写锁
须要提早了解的概念:
锁降级:从写锁变成读锁;
锁升级:从读锁变成写锁。
读锁是能够被多线程共享的,写锁是单线程独占的。也就是说写锁的并发限制比读锁高,这可能就是升级/降级名称的来源。
以下代码会产生死锁,由于同一个线程中,在没有释放读锁的状况下,就去申请写锁,这属于锁升级,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");
以上这段代码虽然不会致使死锁,但没有正确的释放锁。从写锁降级成读锁,并不会自动释放当前线程获取的写锁,仍然须要显示的释放,不然别的线程永远也获取不到写锁。
============如下我会经过一个真实场景下的缓存机制来说解 ReentrantReadWriteLock 实际应用============
首先来看看ReentrantReadWriteLock的javaodoc文档中提供给咱们的一个很好的Cache实例代码案例:
class CachedData { Object data; volatile boolean cacheValid; final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); public 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; } // 在释放写锁以前经过获取读锁降级写锁(注意此时尚未释放写锁) rwl.readLock().lock(); } finally { rwl.writeLock().unlock(); // 释放写锁而此时已经持有读锁 } } try { use(data); } finally { rwl.readLock().unlock(); } } }
以上代码加锁的顺序为:
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过程当中,可能会有其余线程竞争到写锁 或者是更新数据 则得到的数据是其余线程更新的数据,可能会形成数据的污染,即产生脏读的问题。
下面,让咱们来实现真正趋于实际生产环境中的缓存案例:
import java.util.HashMap; import java.util.Map; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; public class CacheDemo { /** * 缓存器,这里假设须要存储1000左右个缓存对象,按照默认的负载因子0.75,则容量=750,大概估计每个节点链表长度为5个 * 那么数组长度大概为:150,又有雨设置map大小通常为2的指数,则最近的数字为:128 */ private Map<String, Object> map = new HashMap<>(128); private ReadWriteLock rwl = new ReentrantReadWriteLock(); public static void main(String[] args) { } public Object get(String id){ Object value = null; rwl.readLock().lock();//首先开启读锁,从缓存中去取 try{ if(map.get(id) == null){ //若是缓存中没有释放读锁,上写锁 rwl.readLock().unlock(); rwl.writeLock().lock(); try{ if(value == null){ //防止多写线程重复查询赋值 value = "redis-value"; //此时能够去数据库中查找,这里简单的模拟一下 } rwl.readLock().lock(); //加读锁降级写锁,不明白的能够查看上面锁降级的原理与保持读取数据原子性的讲解 }finally{ rwl.writeLock().unlock(); //释放写锁 } } }finally{ rwl.readLock().unlock(); //最后释放读锁 } return value; } }