写点什么

通俗易懂读写锁 ReentrantReadWriteLock 的使用

作者:JAVA旭阳
  • 2022-10-17
    浙江
  • 本文字数:3205 字

    阅读完需:约 1 分钟

概述

ReentrantReadWriteLock 不知道大家熟悉吗?其实在实际的项目中用的比较少,反正我所在的项目没有用到过。


ReentrantReadWriteLock 称为读写锁,它提供一个读锁,支持多个线程共享同一把锁。它也提供了一把写锁,是独占锁,和其他读锁或者写锁互斥,表明只有一个线程能持有锁资源。通过两把锁的协同工作,能够最大化的提高读写的性能,特别是读多写少的场景,而往往大部分的场景都是读多写少的。


本文主要讲解 ReentrantReadWriteLock 的使用和应用场景。

ReentrantReadWriteLock 介绍

ReentrantReadWriteLock 实现了 ReadWriteLock 接口,可以获取到读锁(共享锁),写锁(独占锁)。同时,通过构造方法可以创建锁本身是公平锁还是非公锁。


读写锁机制:



线程进入读锁的前提条件:


  • 没有其他线程的写锁

  • 没有写请求,或者有写请求但调用线程和持有锁的线程是同一个线程


进入写锁的前提条件:


  • 没有其他线程的读锁

  • 没有其他线程的写锁


锁升级、降级机制:


我们知道 ReentrantLock 具备可重入的能力,即同一个线程多次获取锁,不引起阻塞,那么ReentrantReadWriteLock关于可重入性是怎么样的呢?


关于这个问题需要引入两个概念,锁升级,锁降级。


  • 锁升级:从读锁变成写锁。

  • 锁降级:从写锁变成读锁;


重入时锁升级不支持:持有读锁的情况下去获取写锁会导致获取写锁永久等待,需要先释放读,再去获得写


重入时锁降级支持:持有写锁的情况下去获取读锁,造成只有当前线程会持有读锁,因为写锁会互斥其他的锁


API 介绍


构造方法:


  • public ReentrantReadWriteLock():默认构造方法,非公平锁

  • public ReentrantReadWriteLock(boolean fair):true 为公平锁


常用 API:


  • public ReentrantReadWriteLock.ReadLock readLock():返回读锁

  • public ReentrantReadWriteLock.WriteLock writeLock():返回写锁

  • public void lock():加锁

  • public void unlock():解锁

  • public boolean tryLock():尝试获取锁


代码范式


  • 加解锁格式


r.lock();try {    // 临界区} finally {  r.unlock();}
复制代码


  • 锁降级


w.lock();try {    r.lock();// 降级为读锁, 释放写锁, 这样能够让其它线程读取缓存    try {        // ...    } finally{      w.unlock();// 要在写锁释放之前获取读锁    }} finally{  r.unlock();}
复制代码

实战案例

验证读读共享模式

@Test    public void readReadMode() throws InterruptedException {        ReentrantReadWriteLock rw = new ReentrantReadWriteLock();        ReentrantReadWriteLock.ReadLock r = rw.readLock();        ReentrantReadWriteLock.WriteLock w = rw.writeLock();
Thread thread0 = new Thread(() -> { r.lock(); try { Thread.sleep(1000); System.out.println("Thread 1 running " + new Date()); } catch (InterruptedException e) { e.printStackTrace(); } finally { r.unlock(); } },"t1");
Thread thread1 = new Thread(() -> { r.lock(); try { Thread.sleep(1000); System.out.println("Thread 2 running " + new Date()); } catch (InterruptedException e) { e.printStackTrace(); } finally { r.unlock(); } },"t2");
thread0.start(); thread1.start();
thread0.join(); thread1.join(); }
复制代码


运行结果:



  • 两个线程同时运行,都获取到了读锁

验证读写互斥模式

@Test    public void readWriteMode() throws InterruptedException {        ReentrantReadWriteLock rw = new ReentrantReadWriteLock();        ReentrantReadWriteLock.ReadLock r = rw.readLock();        ReentrantReadWriteLock.WriteLock w = rw.writeLock();
Thread thread0 = new Thread(() -> { r.lock(); try { Thread.sleep(1000); System.out.println("Thread 1 running " + new Date()); } catch (InterruptedException e) { e.printStackTrace(); } finally { r.unlock(); } },"t1");
Thread thread1 = new Thread(() -> { w.lock(); try { Thread.sleep(1000); System.out.println("Thread 2 running " + new Date()); } catch (InterruptedException e) { e.printStackTrace(); } finally { w.unlock(); } },"t2");
thread0.start(); thread1.start();
thread0.join(); thread1.join(); }
复制代码


运行结果:



  • 两个线程间隔 1 秒,互斥执行

真实缓存例子

什么场景下读多写少? 想必最先想到的就是缓存把,ReentrantReadWriteLock 在缓存场景中就是一个很典型的应用。



缓存更新时,是先清缓存还是先更新数据库?


  • 先清缓存:可能造成刚清理缓存还没有更新数据库,高并发下,其他线程直接查询了数据库过期数据到缓存中,这种情况非常严重,直接导致后续所有的请求缓存和数据库不一致。

  • 先更新据库:可能造成刚更新数据库,还没清空缓存就有线程从缓存拿到了旧数据,这种情况概率比较小,影响范围有限,只对这一次的查询结果有问题。


显而易见,通常情况下,先更新数据库,然后清空缓存。


public class GenericCachedDao {
// 缓存对象,这里用jvm缓存 Map<String, String> cache = new HashMap<>(); // 读写锁 ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
// 读取操作 public String getData(String key) { // 加读锁,防止其他线程修改缓存 readWriteLock.readLock().lock(); try { String value = cache.get(key); // 如果缓存命中,返回 if(value != null) { return value; } } finally { // 释放读锁 readWriteLock.readLock().unlock(); }
//如果缓存没有命中,从数据库中加载 readWriteLock.writeLock().lock(); try { // 细节,为防止重复查询数据库, 再次验证 // 因为get 方法上面部分是可能多个线程进来的, 可能已经向缓存填充了数据 String value = cache.get(key); if(value == null) { // 这里可以改成从数据库查询 value = "alvin"; cache.put(key, value); } return value; } finally { readWriteLock.writeLock().unlock(); } }
// 更新数据 public void updateData(String key, String value) { // 加写锁 readWriteLock.writeLock().lock(); try { // 更新操作TODO
// 清空缓存 cache.remove(key); } finally { readWriteLock.writeLock().unlock(); } }}
复制代码


  • getData 方法是读取操作,先加读锁,从缓存读取,如果没有命中,加写锁,此时其他线程就不能读取了,等写入成功后,释放读锁。

  • updateData 方法是写操作,更新时加写锁,其他线程此时无法读取,然后清空缓存中的旧数据。

总结

本文讲解了 ReentrantReadWriteLock 读写锁常用的 API, 以及通过几个 demo 的演示,讲解了读写锁的使用,希望对大家有帮助。

参考

https://www.cnblogs.com/liang1101/p/6475555.html

发布于: 刚刚阅读数: 3
用户头像

JAVA旭阳

关注

还未添加个人签名 2018-07-18 加入

还未添加个人简介

评论

发布
暂无评论
通俗易懂读写锁ReentrantReadWriteLock的使用_Java_JAVA旭阳_InfoQ写作社区