一.ReadWriteLock是什么
ReadWriteLock是一个接口,提供了readLock和writeLock两种锁的操作机制,一个是只读的锁,一个是写锁。
读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的(排他的)。 每次只能有一个写线程,但是可以有多个线程并发地读数据。
所有读写锁的实现必须确保写操作对读操作的内存影响。换句话说,一个获得了读锁的线程必须能看到前一个释放的写锁所更新的内容。
理论上,读写锁比互斥锁允许对于共享数据更大程度的并发。
与互斥锁相比,读写锁是否能够提高性能取决于读写数据的频率、读取和写入操作的持续时间、以及读线程和写线程之间的竞争。
synchronized和ReentrantLock实现的锁是排他锁,所谓排他锁就是同一时刻只允许一个线程访问共享资源,但是在平时场景中,通常会碰到对于共享资源读多写少的场景。
对于读场景,每次只允许一个线程访问共享资源,显然这种情况使用排他锁效率就比较低下,那么该如何优化呢?
这个时候读写锁就应运而生了,读写锁是一种通用技术,并不是Java特有的。从名字来看,读写锁拥有两把锁,读锁和写锁。
读写锁的特点是:同一时刻允许多个线程对共享资源进行读操作;同一时刻只允许一个线程对共享资源进行写操作;
当进行写操作时,同一时刻其他线程的读操作会被阻塞;当进行读操作时,同一时刻所有线程的写操作会被阻塞。
对于读锁而言,由于同一时刻可以允许多个线程访问共享资源,进行读操作,因此称它为共享锁;而对于写锁而言,同一时刻只允许一个线程访问共享资源,进行写操作,因此称它为排他锁。
在Java中通过ReadWriteLock来实现读写锁。ReadWriteLock是一个接口,ReentrantReadWriteLock是ReadWriteLock接口的具体实现类。
在ReentrantReadWriteLock中定义了两个内部类ReadLock、WriteLock,分别来实现读锁和写锁。
ReentrantReadWriteLock底层是通过AQS来实现锁的获取与释放的,因此ReentrantReadWriteLock内部还定义了一个继承了AQS类的同步组件Sync,
同时ReentrantReadWriteLock还支持公平与非公平性,因此它内部还定义了两个内部类FairSync、NonfairSync,它们继承了Sync。
二.ReadWriteLock能做什么
说到Java并发编程,经常常用的肯定是Synchronized,但是Synchronized存在明显的一个性能问题就是读与读之间互斥,
简言之就是,编程想要实现的最好效果是,可以做到读和读互不影响,读和写互斥,写和写互斥,提高读写的效率,如何实现呢?
对象的方法中一旦加入synchronized修饰,则任何时刻只能有一个线程访问synchronized修饰的方法。
假设有个数据对象拥有写方法与读方法,多线程环境中要想保证数据的安全,需对该对象的读写方法都要加入 synchronized同步块。
这样任何线程在写入时,其它线程无法读取与改变数据;如果有线程在读取时,其他线程也无法读取或写入。
这种方式在写入操作远大于读操作时,问题不大,而当读取远远大于写入时,会造成性能瓶颈,因为此种情况下读取操作是可以同时进行的,而加锁操作限制了数据的并发读取。
ReadWriteLock解决了这个问题,当写操作时,其他线程无法读取或写入数据,而当读操作时,其它线程无法写入数据,但却可以读取数据 。
三.ReadWriteLock原理
在并发场景中用于解决线程安全的问题,几乎会高频率的使用到独占式锁,通常使用java提供的关键字synchronized或者concurrents包中实现了Lock接口的ReentrantLock。
它们都是独占式获取锁,也就是在同一时刻只有一个线程能够获取锁。而在一些业务场景中,大部分只是读数据,写数据很少,
如果仅仅是读数据的话并不会影响数据正确性(出现脏读),而如果在这种业务场景下,依然使用独占锁的话,很显然这将是出现性能瓶颈的地方。
针对这种读多写少的情况,java还提供了另外一个实现Lock接口的ReentrantReadWriteLock(读写锁)。
读写所允许同一时刻被多个读线程访问,但是在写线程访问时,所有的读线程和其他的写线程都会被阻塞。
(1)公平性选择:支持非公平性(默认)和公平的锁获取方式,吞吐量还是非公平优于公平;
(2)重入性:支持重入,读锁获取后能再次获取,写锁获取之后能够再次获取写锁,同时也能够获取读锁;
(3)锁降级:遵循获取写锁,获取读锁再释放写锁的次序,写锁能够降级成为读锁
Lock比传统线程模型中的synchronized方式更加面向对象,与生活中的锁类似,锁本身也应该是一个对象。
两个线程执行的代码片段要实现同步互斥的效果,它们必须用同一个Lock对象。
读写锁:分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由jvm自己控制的,我们只要上好相应的锁即可。
如果你的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁;如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁。总之,读的时候上读锁,写的时候上写锁!
读写锁接口:ReadWriteLock,它的具体实现类为:ReentrantReadWriteLock
在多线程的环境下,对同一份数据进行读写,会涉及到线程安全的问题。
比如在一个线程读取数据的时候,另外一个线程在写数据,而导致前后数据的不一致性;一个线程在写数据的时候,另一个线程也在写,同样也会导致线程前后看到的数据的不一致性。
这时候可以在读写方法中加入互斥锁,任何时候只能允许一个线程的一个读或写操作,而不允许其他线程的读或写操作,这样是可以解决这样以上的问题,但是效率却大打折扣了。
因为在真实的业务场景中,一份数据,读取数据的操作次数通常高于写入数据的操作,而线程与线程间的读读操作是不涉及到线程安全的问题,没有必要加入互斥锁,只要在读-写,写-写期间上锁就行了。
对于以上这种情况,读写锁是最好的解决方案!其中它的实现类:ReentrantReadWriteLock--顾名思义是可重入的读写锁,允许多个读线程获得ReadLock,但只允许一个写线程获得WriteLock。
读写锁的机制:
(1)"读-读" 不互斥——共存
(2)"读-写" 互斥——不能共存
(3)"写-写" 互斥——不能共存
四.ReadWriteLock使用
使用示例:
public class TestReadWriteLock { public static void main(String[] args){ ReadWriteLockDemo rwd = new ReadWriteLockDemo(); //启动100个读线程 for (int i = 0; i < 100; i++) { new Thread(new Runnable() { @Override public void run() { rwd.get(); } }).start(); } //写线程 new Thread(new Runnable() { @Override public void run() { rwd.set((int)(Math.random()*101)); } },"Write").start(); } } class ReadWriteLockDemo{ //模拟共享资源--Number private int number = 0; // 实际实现类--ReentrantReadWriteLock,默认非公平模式 private ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); //读 public void get(){ //使用读锁 readWriteLock.readLock().lock(); try { System.out.println(Thread.currentThread().getName()+" : "+number); }finally { readWriteLock.readLock().unlock(); } } //写 public void set(int number){ readWriteLock.writeLock().lock(); try { this.number = number; System.out.println(Thread.currentThread().getName()+" : "+number); }finally { readWriteLock.writeLock().unlock(); } } }
测试结果如下图:
首先启动读线程,此时number为0;然后某个时刻写线程修改了共享资源number数据,读线程再次读取最新值!
四.ReentrantReadWriteLock使用
ReentrantReadWriteLock会使用两把锁来解决问题,一个读锁,一个写锁。
线程进入读锁的前提条件:
(1)没有其他线程的写锁;
(2)没有写请求,或者有写请求但调用线程和持有锁的线程是同一个线程;
进入写锁的前提条件:
(1)没有其他线程的读锁
(2)没有其他线程的写锁
需要提前了解的概念:
锁降级:从写锁变成读锁;
锁升级:从读锁变成写锁。
读锁是可以被多线程共享的,写锁是单线程独占的。也就是说写锁的并发限制比读锁高,这可能就是升级/降级名称的来源。
如下代码会产生死锁,因为同一个线程中,在没有释放读锁的情况下,就去申请写锁,这属于锁升级,ReentrantReadWriteLock是不支持的。
实际生产环境中的缓存案例:
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; } }
五.总结
(1)Java并发库中ReetrantReadWriteLock实现了ReadWriteLock接口并添加了可重入的特性;
(2)ReetrantReadWriteLock读写锁的效率明显高于synchronized关键字;
(3)ReetrantReadWriteLock读写锁的实现中,读锁使用共享模式;写锁使用独占模式,换句话说,读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的;
(4)ReetrantReadWriteLock读写锁的实现中,需要注意的,当有读锁时,写锁就不能获得;而当有写锁时,除了获得写锁的这个线程可以获得读锁外,其他线程不能获得读锁。