Java并发之搞懂读写锁

 更新时间:2021年11月10日 11:15:03   作者:怎能止步于此  
这篇文章主要介绍了Java并发之读写锁,文中相关实例代码详细,测试可用,具有一定参考价值,需要的朋友可以了解下,希望能够给你带来帮助

ReentrantReadWriteLock

我们来探讨一下java.concurrent.util包下的另一个锁,叫做ReentrantReadWriteLock,也叫读写锁。

实际项目中常常有这样一种场景:

在这里插入图片描述

比如有一个共享资源叫做Some Data,多个线程去操作Some Data,这个操作有读操作也有写操作,并且是读多写少的,那么在没有写操作的时候,多个线程去读Some Data是不会有线程安全问题的,因为线程只是访问,并没有修改,不存在竞争,所以这种情况应该允许多个线程同时读取Some Data。

但是若某个瞬间,线程X正在修改Some Data的时候,那么就不允许其他线程对Some Data做任何操作,否则就会有线程安全问题。

那么针对这种读多写少的场景,J.U.C包提供了ReentrantReadWriteLock,它包含了两个锁:

  • ReadLock:读锁,也被称为共享锁
  • WriteLock:写锁,也被称为排它锁

下面我们看看,线程如果想获取读锁,需要具备哪些条件:

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

再来看一下线程获取写锁的条件:

  • 必须没有其他线程的读锁
  • 必须没有其他线程的写锁

这个比较容易理解,因为写锁是排他的。

来看下面一段代码:

public class ReentrantReadWriteLockTest {
    private Object data;
    //缓存是否有效
    private volatile boolean cacheValid;
    private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    public void processCachedData() {
        rwl.readLock().lock();
        //如果缓存无效,更新cache;否则直接使用data
        if (!cacheValid) {
            //获取写锁前必须释放读锁
            rwl.readLock().unlock();
            rwl.writeLock().lock();
            if (!cacheValid) {
                //更新数据
                data = new Object();
                cacheValid = true;
            }
            //锁降级,在释放写锁前获取读锁
            rwl.readLock().lock();
            //释放写锁,依然持有读锁
            rwl.writeLock().unlock();
        }
        // 使用缓存
        // ...
        // 释放读锁
        rwl.readLock().unlock();
    }
}

这段代码演示的是获取缓存的时候,判断缓存是否过期,如果已经过期就更新缓存,如果没有过期就使用缓存。
可以看到我们先创建了一个读锁,判断如果缓存有效,就可以使用缓存,使用完之后再把读锁释放。如果缓存无效,就更新缓存执行写操作,所以先把读锁给释放掉,然后创建一个写锁,最后更新缓存,更新完缓存后又重新获取了一个读锁并释放掉写锁。

从这段代码里可以看出来,一个线程在拿到写锁之后它还可以继续获得一个读锁。

小结

我们来总结一下ReentrantReadWriteLock的三个特性:

  • 公平性

ReentrantReadWriteLock也可以在初始化时设置是否公平。

  • 可重入性

读锁以及写锁也是支持重入的,比如一个线程拿到写锁后,他依然可以继续拿写锁,同理读锁也可以。

  • 锁降级

要想实现锁降级,只需要先获得写锁,再获得读锁,最后释放写锁,就可以把一个写锁降级为读锁了。但是一个读锁是没有办法升级为写锁的。

最后我们来对比一下ReentrantLock与ReentrantReadWriteLock

  • ReentrantLock:完全互斥
  • ReentrantReadWriteLock:读锁共享,写锁互斥

因此在读多写少的场景下,ReentrantReadWriteLock的性能、吞吐量各方面都会比ReentrantLock要好很多。但是对于写多的场景ReentrantReadWriteLock就不那么明显了。

StampedLock

上面我们已经探讨了ReentrantReadWriteLock能够大幅度提升读多写少场景下的性能,StampedLock是在JDK8引入的,可以认为这是一个ReentrantReadWriteLock的增强版。

那么大家想,既然有了ReentrantReadWriteLock,为什么还要搞一个StampedLock呢?

这是因为ReentrantReadWriteLock在一些特定的场景下存在问题。

比如写线程的“饥饿”问题。
举个例子:假设现在有超级多的线程在操作ReentrantReadWriteLock,执行读操作的线程超级多,而执行写操作的线程很少,而如果这个执行写操作的线程想要拿到写锁,而ReentrantReadWriteLock的写锁是排他的,要想拿到写锁就意味着其他线程不能有读锁也不能有写锁,所以在读线程超级多,写线程超级少的情况下就容易造成写线程饥饿问题,也就是说,执行写操作的线程可能一直抢不到锁,即使可以把公平性设置为true,但是这样又会导致性能的下降。

那么我们看看StampedLock怎么玩:

首先,所有获取锁的方法都会返回stamp,它是一个数字,如果stamp=0说明操作失败了,其他的值表示操作成功。

其次就是所有获取锁的方法,需要用stamp作为参数,参数的值必须和获得锁时返回的stamp一致。

其中StampedLock提供了三种访问模式:

  • Writing模式:类似于ReentrantReadWriteLock的写锁R
  • eding(悲观读模式):类似于ReentrantReadWriteLock的读锁。
  • Optimistic reading:乐观读模式

悲观读模式:在执行悲观读的过程中,不允许有写操作

乐观读模式:在执行乐观读的过程中,允许有写操作

通过介绍我们可以发现,StampedLock中的悲观读与乐观读和我们操作数据库中的悲观锁、乐观锁有一定的相似之处。

此外StampedLock还提供了读锁和写锁相互转换的功能:

我们知道ReentrantReadWriteLock的写锁是可以降级为读锁的,但是读锁没办法升级为写锁,而StampedLock它提供了读锁和写锁之间互相转换的功能。

最后,StampedLock是不可重入的,这也是和ReentrantReadWriteLock的一个区别。

读过源码的同学可能知道,在StampedLock源码里有一段注释:

在这里插入图片描述

我们来看一下这段注释,他写的非常经典,演示了StampedLock API如何使用。

class Point {
    private double x, y;
    private final StampedLock sl = new StampedLock();
    void move(double deltaX, double deltaY) { // an exclusively locked method
      //添加写锁
      long stamp = sl.writeLock();
      try {
        x += deltaX;
        y += deltaY;
      } finally {
        //释放写锁
        sl.unlockWrite(stamp);
      }
    }
    double distanceFromOrigin() { // A read-only method
      //获得一个乐观锁
      long stamp = sl.tryOptimisticRead();
      // 假设(x,y)=(10,10)
      // 但是这是一个乐观读锁,(x,y)可能被其他线程修改为(20,20)
      double currentX = x, currentY = y;
      //因此这里要验证获得乐观锁后,有没有发生写操作
      if (!sl.validate(stamp)) {
         stamp = sl.readLock();
         try {
           currentX = x;
           currentY = y;
         } finally {
            sl.unlockRead(stamp);
         }
      }
      return Math.sqrt(currentX  currentX + currentY  currentY);
    }
    void moveIfAtOrigin(double newX, double newY) { // upgrade
      // Could instead start with optimistic, not read mode
      long stamp = sl.readLock();
      try {
        while (x == 0.0 && y == 0.0) {
          long ws = sl.tryConvertToWriteLock(stamp);
          if (ws != 0L) {
            stamp = ws;
            x = newX;
            y = newY;
            break;
          }
          else {
            sl.unlockRead(stamp);
            stamp = sl.writeLock();
          }
        }
      } finally {
        sl.unlock(stamp);
      }
    }
}

在这里插入图片描述

这个类有三个方法,move方法用来移动一个点的坐标,instanceFromOrigin用来计算这个点到原点的距离,moveIfAtOrigin表示当这个点位于原点的时候用来移动这个点的坐标。

我们来分析一下源码:

move方法是一个纯粹的写操作,在操作之前添加写锁,操作结束释放写锁;

instanceOrigin首先获得一个乐观锁,然后开始读数据,我们假设(x,y)=(10,10),但是这是一个乐观读锁,(x,y)可能被其他线程修改为(20,20),所以他会验证获得乐观锁后,有没有发生写操作,如果validate结果为true的话,表示没有发生过写操作,如果发生过写操作,那么就会改用悲观读锁重读数据,然后计算结果,当然最后要把锁释放掉。

最后moveIfAtOrigin方法也比较简单,主要演示了怎么从悲观读锁转换成写锁。

小结

StampedLock主要通过乐观读的方式提升性能,同时也解决了写线程的饥饿问题,但是有得必有失,我们从示例代码中不难看出,StampedLock使用起来要比ReentrantReadWriteLock复杂很多,所以使用者要在性能和复杂度之间做一个取舍。

总结

本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注脚本之家的更多内容!

相关文章

  • Java几个重要的关键字详析

    Java几个重要的关键字详析

    这篇文章主要介绍了Java几个重要的关键字详析,文章围绕主题展开详细的内容介绍,具有一定的参考一下,需要的小伙伴可以参考一下,希望对你的学习有所帮助
    2022-07-07
  • JVM的GC日志及运行参数解读

    JVM的GC日志及运行参数解读

    这篇文章主要为大家介绍了JVM的GC日志及运行参数解读,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-09-09
  • 如何利用Spring把元素解析成BeanDefinition对象

    如何利用Spring把元素解析成BeanDefinition对象

    这篇文章主要介绍了如何利用Spring把元素解析成BeanDefinition对象,文章围绕主题展开详细的内容介绍,具有一定的参考价值,需要的小伙伴可以参考一下
    2022-08-08
  • logstash将mysql数据同步到elasticsearch方法详解

    logstash将mysql数据同步到elasticsearch方法详解

    这篇文章主要为大家介绍了logstash将mysql数据同步到elasticsearch方法详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-12-12
  • 基于java中的流程控制语句总结(必看篇)

    基于java中的流程控制语句总结(必看篇)

    下面小编就为大家带来一篇基于java中的流程控制语句总结(必看篇)。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-06-06
  • springboot整合shiro登录失败次数限制功能的实现代码

    springboot整合shiro登录失败次数限制功能的实现代码

    这篇文章主要介绍了springboot整合shiro-登录失败次数限制功能,实现此功能如果是防止坏人多次尝试,破解密码的情况,所以要限制用户登录尝试次数,需要的朋友可以参考下
    2018-09-09
  • Java开发环境配置JDK超详细整理(适合新手入门)

    Java开发环境配置JDK超详细整理(适合新手入门)

    这篇文章主要给大家介绍了关于Java开发环境配置JDK超详细整理的相关资料,非常适合新手入门,JDK是Java语言的软件开发工具包,主要用于移动设备、嵌入式设备上的java应用程序,需要的朋友可以参考下
    2023-11-11
  • Java设计模式中的观察者模式

    Java设计模式中的观察者模式

    观察者模式定义对象之间的一种一对多的依赖关系,使得每当一个对象的状态发生变化时,其相关的依赖对象都可以得到通知并被自动更新。主要用于多个不同的对象对一个对象的某个方法会做出不同的反应
    2023-02-02
  • Java垃圾回收之复制算法详解

    Java垃圾回收之复制算法详解

    今天小编就为大家分享一篇关于Java垃圾回收之复制算法详解,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧
    2018-10-10
  • 如何使用 Spring Boot 搭建 WebSocket 服务器实现多客户端连接

    如何使用 Spring Boot 搭建 WebSocket 服务器实现多客户端连接

    本文介绍如何使用SpringBoot快速搭建WebSocket服务器,实现多客户端连接和消息广播,WebSocket协议提供全双工通信,SpringBoot通过@ServerEndpoint简化配置,支持实时消息推送,适用于聊天室或通知系统等应用场景
    2024-11-11

最新评论