首页 > 编程语言 >并发编程系列 - ReadWriteLock

并发编程系列 - ReadWriteLock

时间:2023-09-21 10:46:07浏览次数:51  
标签:缓存 写锁 ReadWriteLock 读写 编程 并发 读锁 线程 lock

实际工作中,为了优化性能,我们经常会使用缓存,例如缓存元数据、缓存基础数据等,这就是一种典型的读多写少应用场景。缓存之所以能提升性能,一个重要的条件就是缓存的数据一定是读多写少的,例如元数据和基础数据基本上不会发生变化(写少),但是使用它们的地方却很多(读多)。

针对读多写少这种并发场景,Java SDK并发包提供了读写锁——ReadWriteLock,非常容易使用,并且性能很好。在并发编程中,有时我们需要处理多个线程同时读取共享资源的情况,同时还要保证在有写操作时,对资源的访问是互斥的。这就是读写锁(ReadWriteLock)的应用场景。

什么是读写锁?

读写锁是一种锁机制,它允许多个线程可同时读取共享资源,但在写操作时需要互斥。读写锁将读操作与写操作分开,以提高并发性和性能。

ReadWriteLock的特点

  • 多个线程可同时读取:在没有写操作的情况下,多个线程可以并发地读取共享资源,从而提升读取操作的性能。

  • 写操作是互斥的:写操作会独占锁,确保在写操作进行时没有其他线程可以读取或写入共享资源。

  • 读写操作之间互斥:在写操作进行时,其他线程不能读取或写入,以保证数据的一致性。

读写锁与互斥锁的一个重要区别就是 读写锁允许多个线程同时读共享变量,而互斥锁是不允许的,这是读写锁在读多写少场景下性能优于互斥锁的关键。但 读写锁的写操作是互斥的,当一个线程在写共享变量的时候,是不允许其他线程执行写操作和读操作。

如何使用ReadWriteLock

Java提供了java.util.concurrent.locks包中的ReentrantReadWriteLock类来实现读写锁。下面是一个简单的例子:

`import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockExample {
    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    private int sharedData = 0;

    public int readData() {
        lock.readLock().lock();
        try {
            return sharedData;
        } finally {
            lock.readLock().unlock();
        }
    }

    public void writeData(int newData) {
        lock.writeLock().lock();
        try {
            sharedData = newData;
        } finally {
            lock.writeLock().unlock();
        }
    }
}

在上面的例子中,我们创建了一个ReentrantReadWriteLock实例作为读写锁。使用readLock()方法获取读锁,writeLock()方法获取写锁。

在读取共享资源时,我们需要先获取读锁,然后执行读操作,最后释放读锁。在写入共享资源时,我们需要先获取写锁,然后执行写操作,最后释放写锁。

要注意的是,在使用读写锁时,应该根据实际需求合理地使用读锁和写锁,以便提升并发性和性能。

读写锁的优势与适用场景

  • 读多写少:当有大量读取操作,而写操作较少的情况下,读写锁可以提高系统的并发性和性能。

  • 数据一致性要求较低:如果对共享资源的一致性要求不高,即使在读写操作之间出现一定的延迟或不一致,也不会对系统产生严重影响。

  • 提升并发性和性能:读写锁通过允许多个线程同时读取共享资源,以及在写操作时互斥地访问资源,可以提高系统的并发性和性能。

快速实现一个缓存

下面我们就实践起来,用ReadWriteLock快速实现一个通用的缓存工具类。

在下面的代码中,我们声明了一个Cache<K, V>类,其中类型参数K代表缓存里key的类型,V代表缓存里value的类型。缓存的数据保存在Cache类内部的HashMap里面,HashMap不是线程安全的,这里我们使用读写锁ReadWriteLock 来保证其线程安全。ReadWriteLock 是一个接口,它的实现类是ReentrantReadWriteLock,通过名字你应该就能判断出来,它是支持可重入的。下面我们通过rwl创建了一把读锁和一把写锁。

Cache这个工具类,我们提供了两个方法,一个是读缓存方法get(),另一个是写缓存方法put()。读缓存需要用到读锁,读锁的使用和前面我们介绍的Lock的使用是相同的,都是try{}finally{}这个编程范式。写缓存则需要用到写锁,写锁的使用和读锁是类似的。这样看来,读写锁的使用还是非常简单的。

class Cache<K,V> {
  final Map<K, V> m =
    new HashMap<>();
  final ReadWriteLock rwl =
    new ReentrantReadWriteLock();
  // 读锁
  final Lock r = rwl.readLock();
  // 写锁
  final Lock w = rwl.writeLock();
  // 读缓存
  V get(K key) {
    r.lock();
    try { return m.get(key); }
    finally { r.unlock(); }
  }
  // 写缓存
  V put(K key, V value) {
    w.lock();
    try { return m.put(key, v); }
    finally { w.unlock(); }
  }
}

如果你曾经使用过缓存的话,你应该知道 使用缓存首先要解决缓存数据的初始化问题。缓存数据的初始化,可以采用一次性加载的方式,也可以使用按需加载的方式。

如果源头数据的数据量不大,就可以采用一次性加载的方式,这种方式最简单(可参考下图),只需在应用启动的时候把源头数据查询出来,依次调用类似上面示例代码中的put()方法就可以了。

缓存一次性加载示意图

如果源头数据量非常大,那么就需要按需加载了,按需加载也叫懒加载,指的是只有当应用查询缓存,并且数据不在缓存里的时候,才触发加载源头相关数据进缓存的操作。下面你可以结合文中示意图看看如何利用ReadWriteLock 来实现缓存的按需加载。

缓存按需加载示意图

实现缓存的按需加载

文中下面的这段代码实现了按需加载的功能,这里我们假设缓存的源头是数据库。需要注意的是,如果缓存中没有缓存目标对象,那么就需要从数据库中加载,然后写入缓存,写缓存需要用到写锁,所以在代码中的⑤处,我们调用了 w.lock() 来获取写锁。

另外,还需要注意的是,在获取写锁之后,我们并没有直接去查询数据库,而是在代码⑥⑦处,重新验证了一次缓存中是否存在,再次验证如果还是不存在,我们才去查询数据库并更新本地缓存。为什么我们要再次验证呢?

class Cache<K,V> {
  final Map<K, V> m =
    new HashMap<>();
  final ReadWriteLock rwl =
    new ReentrantReadWriteLock();
  final Lock r = rwl.readLock();
  final Lock w = rwl.writeLock();

  V get(K key) {
    V v = null;
    //读缓存
    r.lock();         ①
    try {
      v = m.get(key); ②
    } finally{
      r.unlock();     ③
    }
    //缓存中存在,返回
    if(v != null) {   ④
      return v;
    }
    //缓存中不存在,查询数据库
    w.lock();         ⑤
    try {
      //再次验证
      //其他线程可能已经查询过数据库
      v = m.get(key); ⑥
      if(v == null){  ⑦
        //查询数据库
        v=省略代码无数
        m.put(key, v);
      }
    } finally{
      w.unlock();
    }
    return v;
  }
}

原因是在高并发的场景下,有可能会有多线程竞争写锁。假设缓存是空的,没有缓存任何东西,如果此时有三个线程T1、T2和T3同时调用get()方法,并且参数key也是相同的。那么它们会同时执行到代码⑤处,但此时只有一个线程能够获得写锁,假设是线程T1,线程T1获取写锁之后查询数据库并更新缓存,最终释放写锁。此时线程T2和T3会再有一个线程能够获取写锁,假设是T2,如果不采用再次验证的方式,此时T2会再次查询数据库。T2释放写锁之后,T3也会再次查询一次数据库。而实际上线程T1已经把缓存的值设置好了,T2、T3完全没有必要再次查询数据库。所以,再次验证的方式,能够避免高并发场景下重复查询数据的问题。

读写锁的升级与降级

上面按需加载的示例代码中,在①处获取读锁,在③处释放读锁,那是否可以在②处的下面增加验证缓存并更新缓存的逻辑呢?详细的代码如下。

//读缓存
r.lock();         ①
try {
  v = m.get(key); ②
  if (v == null) {
    w.lock();
    try {
      //再次验证并更新缓存
      //省略详细代码
    } finally{
      w.unlock();
    }
  }
} finally{
  r.unlock();     ③
}

这样看上去好像是没有问题的,先是获取读锁,然后再升级为写锁,对此还有个专业的名字,叫 锁的升级。可惜ReadWriteLock并不支持这种升级。在上面的代码示例中,读锁还没有释放,此时获取写锁,会导致写锁永久等待,最终导致相关线程都被阻塞,永远也没有机会被唤醒。锁的升级是不允许的,这个你一定要注意。

不过,虽然锁的升级是不允许的,但是锁的降级却是允许的。以下代码来源自ReentrantReadWriteLock的官方示例,略做了改动。你会发现在代码①处,获取读锁的时候线程还是持有写锁的,这种锁的降级是支持的。

class CachedData {
  Object data;
  volatile boolean cacheValid;
  final ReadWriteLock rwl =
    new ReentrantReadWriteLock();
  // 读锁
  final Lock r = rwl.readLock();
  //写锁
  final Lock w = rwl.writeLock();

  void processCachedData() {
    // 获取读锁
    r.lock();
    if (!cacheValid) {
      // 释放读锁,因为不允许读锁的升级
      r.unlock();
      // 获取写锁
      w.lock();
      try {
        // 再次检查状态
        if (!cacheValid) {
          data = ...
          cacheValid = true;
        }
        // 释放写锁前,降级为读锁
        // 降级是可以的
        r.lock(); ①
      } finally {
        // 释放写锁
        w.unlock();
      }
    }
    // 此处仍然持有读锁
    try {use(data);}
    finally {r.unlock();}
  }
}

总结

读写锁与ReentrantLock类似,还支持公平模式和非公平模式。读锁和写锁都实现了java.util.concurrent.locks.Lock接口,因此除了支持lock()方法外,还支持tryLock()、lockInterruptibly()等方法。但是需要注意的是,只有写锁支持条件变量,而读锁不支持条件变量,因此读锁调用newCondition()会抛出UnsupportedOperationException异常。

今天我们使用了ReadWriteLock实现了一个简单的缓存。尽管该缓存解决了初始化问题,但未解决缓存数据与源数据的同步问题,即确保缓存数据与源数据的一致性。解决数据同步问题最简单的方法之一是使用超时机制。超时机制意味着缓存中加载的数据并不长期有效,而是有一定时效性。当缓存数据超过时效时间后,数据在缓存中失效。对于访问失效的缓存数据,会触发重新从源数据加载到缓存中。

当然,也可以在源数据发生变化时快速通知缓存,但这取决于具体的场景。例如,在MySQL作为数据源时,可以通过实时解析binlog来检测数据是否发生变化,一旦变化就将最新数据推送给缓存。另外,还有一些方案采用了数据库和缓存双写的策略。

顶尖架构师栈

关注回复关键字

【C01】超10G后端学习面试资源

【IDEA】最新IDEA激活工具和码及教程

【JetBrains软件名】 最新软件激活工具和码及教程

工具&码&教程

转载于:https://mp.weixin.qq.com/s/b8dg-6y9_TTy4K05mKsmyw

标签:缓存,写锁,ReadWriteLock,读写,编程,并发,读锁,线程,lock
From: https://www.cnblogs.com/dc-s/p/17719313.html

相关文章

  • java并发
    符合死锁的四个条件:互斥条件:一个时刻一个线程一个资源请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。不剥夺条件:线程已获得的资源,在未用完之前,不能被其他线程剥夺。循环等待条件:若干线程形成头尾相接的循环等待资源关系。如何预防和避免线程死......
  • 【赠书活动 -第01期】-〖Java编程思想(第四版)〗
    【赠书活动-第01期】-〖Java编程思想(第四版)〗活动时间:2023年9月21日~2023年9月30日参与方式:https://m.hlcode.cn/?id=NK1fWUR......
  • SHELL编程开发:如何轻松一键部署Nginx脚本
    实现思路下载Nginx源码包解压源码包进入解压后的目录配置编译选项编译并安装Nginx启动Nginx服务代码实现以下是脚本内容及注释:#!/bin/bash#一键部署Nginx脚本#下载Nginx源码包wgethttp://nginx.org/download/nginx-1.20.1.tar.gz#解压源码包tar-zxvfnginx-1.20.1.tar.......
  • 论文查重-第一次个人编程
    1、github链接:https://github.com/lanzeye7/lanzeye72、PSP表格PSP2.1PersonalSoftwareProcessStages预估耗时(分钟)实际耗时(分钟)Planning计划 60 85·Estimate·估计这个任务需要多少时间 180 210Development开发......
  • 日常编程奇葩又常规问题总结
    相信大家在日常编程中遇到过不少奇葩问题,第一反应就是不可能,怎么可能会出现这个问题呢?于是抓耳挠腮,带着充满疑惑的问题,四处寻找答案。其实遇到问题并不可怕,可怕的是我们缺少解决问题的信心,正所谓只要思想不滑坡,办法总比困难多。解决问题才是打工人的价值所在,也是个人价值展现......
  • Java学习之路--GUI编程06
    packagecom.gui.lesson06;importjavax.swing.*;importjava.awt.*;//2023.3.25/3.26GUI编程--下拉框学习(Combobox)//这个程序最终运行结果不美观,正常情况下下拉框我们放在一个面板里面再添加到容器中。这里就只是演示下拉框是什么样子publicclassTestComboboxDemo01extendsJ......
  • Java学习之路--GUI编程--贪吃蛇小游戏
    贪吃蛇小游戏界面实现四步走1.定义数据2.面板里将数据画上去3.监听事件键盘事件packagecom.gui.snake;importjavax.swing.*;importjava.net.URL;//2023.3.28GUI编程--贪吃蛇小游戏的实现--数据中心(只有一堆图片的数据)--贪吃蛇各个部分的导入--图片文件夹有两种导入......
  • 《Linux命令行与shell脚本编程大全.第3版》电子书PDF+源代码
    精通Linux命令行与shell脚本编程,尽在本书中本书是关于Linux命令行和shell命令的全面参考资料,涵盖详尽的动手教程和实际应用指南,并提供相关参考信息和背景资料,带你从Linux命令行基础入手,直到写出自己的shell。时隔四年后的这一版本,针对Linux的新特性和实践,进行了全面更新:使用......
  • Java学习之路--GUI编程03
    packagecom.gui.lesson03;importjava.awt.*;importjava.awt.event.KeyAdapter;importjava.awt.event.KeyEvent;//2023.3.20GUI编程--键盘监听学习publicclassTestKeyListener{publicstaticvoidmain(String[]args){newKeyFrame();}}//键盘监听类cla......
  • Java学习之路--GUI编程04
    packagecom.gui.lesson04;importjavax.swing.*;importjava.awt.*;importjava.awt.event.ActionEvent;importjava.awt.event.ActionListener;//2023.3.21GUI编程--dialog(弹窗)学习publicclassDialogDemoextendsJFrame{publicDialogDemo(){this.setVisibl......