ReentrantReadWriteLock和StampedLock
一、ReentrantReadWriteLock可重入读写锁
1、ReentrantReadWriteLock是什么?
一个资源能够被多个读线程访问,或者被一个写线程访问,但是不能同时存在读写线程;
其实是2把锁,它允许“读读”共享,但是“读写/写写”是互斥的(排他);
2、为什么会出现ReentrantReadWriteLock?
在一些业务场景中,大部分是读数据,写数据很少,如果仅仅是读数据的话并不会影响数据正确性,如果在读数据很频繁情况下,依然使用独占式锁,会大大降低性能。
所以在这种读多写少的情况下,Java提供另一个lock接口的实现子类ReentrantReadWriteLock(可重入读写锁)。
读写锁允许同一时刻被多个读线程访问,但是只要有写线程访问时,所有的读线程与其他的写线程均会阻塞。
3、ReentrantReadWriteLock有哪些特点?
-
可重入性:
可重入就是同一个线程可以重复加锁,每次加锁的时候count值加1,每次释放锁的时候count减1,直到count为0,其他的线程才可以再次获取(即自己可再次进入获取锁);
-
读写分离:
线程写数据的时候加锁是为了确保数据的准确性,但是线程读数据的时候再加锁就会大大降低效率,这时候怎么办呢?那就对写数据和读数据分开,加上两把不同的锁,不仅保证了正确性,还能提高效率。
-
允许锁降级(写权限高于读权限):
如果一个线程占有了写锁,在不释放写锁的情况下,它还能占有读锁,即写锁降级为读锁。
锁降级是为了让当前线程感知到数据的变化,目的是保证数据可见性;
怎么理解呢?
比如我一个线程获取到“写锁”了,这个时候我写完数据,我可以直接获取到此资源的“读锁”,从而判断我数据已经修改成功了;
-
不可锁升级
很简单,因为“读权限”小于“写权限”,所以无法直接从“读锁”升级为“写锁”!
二、Demo演示读写锁的锁升级过程
1、先占“读锁”,后占“写锁”
public class ReentrantReadWriteLockDemo { public static void main(String[] args) { //默认为false非公平锁,非公平锁效率高于公平锁 //区别:公平锁要索淼队列,然后站到最后,唤醒最前;而非公平锁,直接就过来抢; ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(false); //先占读锁,后写锁,被阻塞,因为“读锁”无法升级为写锁 rwLock.readLock().lock(); System.out.println("我获取到读锁了"); rwLock.writeLock().lock(); System.out.println("我获取到写锁了"); } }
测试结果:
“读锁”无法升级为“写锁”,被阻塞!
2、先占“写锁”,后占“读锁”
public class ReentrantReadWriteLockDemo { public static void main(String[] args) { //默认为false非公平锁,非公平锁效率高于公平锁 //区别:公平锁要索淼队列,然后站到最后,唤醒最前;而非公平锁,直接就过来抢; ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(false); //先占写锁,后读锁,畅通,因为“写锁”可以降级为“读锁” rwLock.writeLock().lock(); System.out.println("我获取到写锁了"); rwLock.readLock().lock(); System.out.println("我获取到读锁了"); } }
测试结果:
“写锁”可以顺利降级为“读锁”,线程不阻塞!
三、StampedLock——比ReentrantReadWriteLock更快的锁
StampedLock是JDK1.8中新增的一个读写锁,也是对JDK1.5中的读写锁ReentrantReadWriteLock的优化
1、为什么会出现StampedLock?
ReentrantReadWriteLock已经对传统的锁进行了优化,实现了读写分类、锁降级等优化,但是还是会存在一定的问题:
-
“写锁”为“独占锁”,想要获取,必须其它锁都不存在了才可以,无论是“读锁”还是“写锁”,这是一个“悲观锁”;
-
而且,当“读操作很多,写操作很少”时,线程有可能会遭遇“锁饥饿”问题;
锁饥饿:
ReentrantReadWriteLock实现了读写分离,但是一旦读操作比较多的时候,想要获取写锁就变得比较困难了,
假如当前1000个线程,999个读,1个写,有可能999个读取线程长时间抢到了锁,那1个写线程就悲剧了
因为当前有可能会一直存在读锁,而无法获得写锁,根本没机会写,o(╥﹏╥)o
缓解方案:
使用“公平”策略,即new ReentrantReadWriteLock(true)创建公平的“可重入读写锁”,大家FIFO,但是“公平”的代价是“牺牲系统吞吐量”!
2、StampedLock闪亮登场
StampedLock控制锁主要有三种模式(读锁、写锁、乐观读)
stampedLock.readLock(); //类似于原读锁 stampedLock.writeLock(); //类似于原写锁 stampedLock.tryOptimisticRead(); //乐观读,我觉得,我读取的时候,数据肯定不会被修改
StampedLock采取乐观获取读锁后,即使再有其他线程尝试获取写锁时也不会被阻塞,这其实是对读锁的优化,
但是,为了准确,在获取乐观读锁结果后,还需要对结果进行校验;
四、代码验证StampedLock的乐观读模式
1、先验证“传统悲观读”
——读操作需耗时4s,我们让“写操作”在“读操作”开始后的1s内执行
public class StampedLockDemo { int number = 37; StampedLock stampedLock = new StampedLock(); //1、先实现一个悲观读锁————“读锁”未释放前,“写锁”是不能介入的,只能被阻塞 private void read(){ long stamp = stampedLock.readLock(); //传统的悲观读模式 System.out.println(Thread.currentThread().getName()+"\t 进入到悲观读代码块,共计需要4秒钟读完"); for (int i = 1; i <=4; i++) { //暂停几秒钟线程 try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"\t 正在读取中..."+i); } try { int result = number; System.out.println(Thread.currentThread().getName()+"\t 获得变量值result: "+result); System.out.println("写线程没有修改,不可介入。因为stampedLock.readLock(),不可以写入,读写互斥"); }catch (Exception e){ e.printStackTrace(); }finally { stampedLock.unlockRead(stamp); } } //普通写 private void write(){ long stamp = stampedLock.writeLock(); //普通写锁 try { System.out.println(Thread.currentThread().getName()+"\t 写线程开始修改"); number = number + 13; System.out.println(Thread.currentThread().getName()+"\t 写线程结束修改"); }catch (Exception e){ e.printStackTrace(); }finally { stampedLock.unlockWrite(stamp); } } public static void main(String[] args) { StampedLockDemo resource = new StampedLockDemo(); new Thread(() -> { resource.read(); //先悲观读 }, "readthread").start(); //暂停1秒钟线程,然后让写锁参与进来修改数据 try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(() ->{ resource.write(); //“写操作”在“读操作”开始后的1秒钟,被启动 },"writethread").start(); } }
执行结果:
结论:
写操作进入时,尝试获取“写锁”,但是被“悲观读锁”阻塞在外,只有等“悲观读锁”完全结束,释放锁之后,“写锁”才被获取到,执行写操作;
2、再验证“尝试乐观读”
public class StampedLockDemo { int number = 37; StampedLock stampedLock = new StampedLock(); //尝试乐观读————“读锁”未释放前,如果有“写锁”想进入,也是可以的,直接进行修改操作,不需要被阻塞 private void tryOptimisticRead(){ long stamp = stampedLock.tryOptimisticRead(); int result = number; //间隔4秒钟,我们很乐观的认为没有其他修改来修改,标志位不动,美好设想。 System.out.println("4秒前stampedLock.validate值(true无修改,false有修改)"+"\t"+stampedLock.validate(stamp)); for (int i = 1; i <=4; i++) { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"\t正在读取中..."+i+"stampedLock.validate值(true无修改,false有修改)"+"\t"+stampedLock.validate(stamp)); } if (!stampedLock.validate(stamp)){ //需要验证一下,是否被串改 System.out.println("-----有人动过数据了,只能重新来一次"); stamp = stampedLock.readLock(); result = number; System.out.println("重新读取一次最新值result: "+result); stampedLock.unlockRead(stamp); } System.out.println(Thread.currentThread().getName()+"\t final result: "+result); } //普通写 private void write(){ long stamp = stampedLock.writeLock(); //普通写锁 try { System.out.println(Thread.currentThread().getName()+"\t 写线程开始修改"); number = number + 13; System.out.println(Thread.currentThread().getName()+"\t 写线程结束修改"); }catch (Exception e){ e.printStackTrace(); }finally { stampedLock.unlockWrite(stamp); } } public static void main(String[] args) { StampedLockDemo resource = new StampedLockDemo(); new Thread(() -> { resource.tryOptimisticRead(); }, "readthread").start(); //暂停1秒钟线程,然后让写锁参与进来修改数据 try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(() ->{ resource.write(); //“写操作”在“读操作”开始后的1秒钟,被启动 },"writethread").start(); } }
执行结果:
结论:
即使“读锁”已经被占用,有“读操作”存在,但是由于是“乐观读”,有“写操作”需要获取“写锁”的时候,依然可以进入获取,并顺利修改了数据;
但是当“读锁”持有者真正准备读取数据的时候,先检验手上的stamp对应的锁是否被篡改,说过为false,则已经被篡改,则从新使用“传统readLock”读取一次;
——这种性能高的原因是,被篡改的可能性很小,毕竟,写操作很少;而当有“写操作”需要获取“写锁”时,也不需要阻塞,而是直接获取!