Java并发之搞懂读写锁

目录
  • ReentrantReadWriteLock
    • 小结
  • StampedLock
    • 小结
  • 总结

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复杂很多,所以使用者要在性能和复杂度之间做一个取舍。

总结

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

(0)

相关推荐

  • Java编程读写锁详解

    ReadWriteLock也是一个接口,提供了readLock和writeLock两种锁的操作机制,一个资源可以被多个线程同时读,或者被一个线程写,但是不能同时存在读和写线程. 基本规则: 读读不互斥 读写互斥 写写互斥 问题: 既然读读不互斥,为何还要加读锁 答: 如果只是读,是不需要加锁的,加锁本身就有性能上的损耗 如果读可以不是最新数据,也不需要加锁 如果读必须是最新数据,必须加读写锁 读写锁相较于互斥锁的优点仅仅是允许读读的并发,除此之外并无其他. 结论: 读写锁能够保证读取数据的 严格

  • Java并发编程之重入锁与读写锁

    重入锁 重入锁,顾名思义,就是支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁.重进入是指任意线程在获取到锁之后能够再次获取该锁而不会被锁阻塞,该特性的实现需要解决以下两个问题. 1.线程再次获取锁.锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取. 2.锁的最终释放.线程重复n次获取了锁,随后在第n次释放该锁后,其他线程能够获取到该锁.锁的最终释放要求锁对于获取进行计数自增,计数表示当前锁被重复获取的次数,而锁被释放时,计数自减,当计数等于0时表示锁已经成功释放

  • Java并发编程之显示锁ReentrantLock和ReadWriteLock读写锁

    在Java5.0之前,只有synchronized(内置锁)和volatile. Java5.0后引入了显示锁ReentrantLock. ReentrantLock概况 ReentrantLock是可重入的锁,它不同于内置锁, 它在每次使用都需要显示的加锁和解锁, 而且提供了更高级的特性:公平锁, 定时锁, 有条件锁, 可轮询锁, 可中断锁. 可以有效避免死锁的活跃性问题.ReentrantLock实现了 Lock接口: 复制代码 代码如下: public interface Lock {  

  • Java并发编程之ReadWriteLock读写锁的操作方法

    1.ReadWriteLock介绍 为什么我们有了Lock,还要用ReadWriteLock呢.我们对共享资源加锁之后,所有的线程都将会等待.Lock读操作也锁,写操作也会锁,而对共享资源读的时候,其实是不用加锁的.当然读写同时存在的情况也会有. 比如我们数据库常用操作有增删改查,增删改都是写操作,写操作必须加锁,而读操作可以共享.不是所有的操作都需要加锁. 为了进一步提高复用性和粒度,写操作独占,读操作共享,不加锁. ReadWriteLock管理一组锁,一个是只读的锁,一个是写锁.读锁可以在

  • java多线程-读写锁原理

    Java5 在 java.util.concurrent 包中已经包含了读写锁.尽管如此,我们还是应该了解其实现背后的原理. 读/写锁的 Java 实现(Read / Write Lock Java Implementation) 读/写锁的重入(Read / Write Lock Reentrance) 读锁重入(Read Reentrance) 写锁重入(Write Reentrance) 读锁升级到写锁(Read to Write Reentrance) 写锁降级到读锁(Write to

  • Java并发之搞懂读写锁

    目录 ReentrantReadWriteLock 小结 StampedLock 小结 总结 ReentrantReadWriteLock 我们来探讨一下java.concurrent.util包下的另一个锁,叫做ReentrantReadWriteLock,也叫读写锁. 实际项目中常常有这样一种场景: 比如有一个共享资源叫做Some Data,多个线程去操作Some Data,这个操作有读操作也有写操作,并且是读多写少的,那么在没有写操作的时候,多个线程去读Some Data是不会有线程安全问

  • Java 重入锁和读写锁的具体使用

    重入锁 重入锁 ReentrantLock,顾名思义,就是支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁.除此之外,该锁还支持获取锁时的公平和非公平性选择 所谓不支持重进入,可以考虑如下场景:当一个线程调用 lock() 方法获取锁之后,如果再次调用 lock() 方法,则该线程将会被自己阻塞,原因是在调用 tryAcquire(int acquires) 方法时会返回 false,从而导致线程阻塞 synchronize 关键字隐式的支持重进入,比如一个 synchronize 修

  • java并发编程StampedLock高性能读写锁详解

    目录 一.读写锁 二.悲观读锁 三.乐观读 一.读写锁 在我的<java并发编程>上一篇文章中为大家介绍了<ReentrantLock读写锁>,ReentrantReadWriteLock可以保证最多同时有一个线程在写数据,或者可以同时有多个线程读数据,但读写不能同时进行. 比如你正在做的是日志,有一个线程正在做写操作,但是在写日志的时候你可能需要把日志集中转移到集中管理日志服务,但是此时读线程不能读数据(因为无法获取读锁).面对这个需求,ReentrantReadWriteLoc

  • Java基础:彻底搞懂java多线程

    目录 进程与线程 使用多线程的优势 线程的状态 创建线程 线程中断 总结 进程与线程 进程 进程是操作系统结构的基础,是程序在一个数据集合上运行的过程,是系统进行资源分配和调度的基本单位.进程可以被看作程序的实体,同样,它也是程序的容器. 线程 线程是操作系统调度的最小单元,也叫作轻量级进程.在一个进程中可以创建多个线程,这些线程都拥有各自的计数器.堆栈和局部变量等属性. 使用多线程的优势 使用多线程可以减少程序的响应时间 如果某个操作很耗时,或者陷入长时间的等待,此时程序将不会响应鼠标和键盘等

  • Java多线程编程之读写锁ReadWriteLock用法实例

    读写锁:分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由jvm自己控制的,你只要上好相应的锁即可.如果你的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁:如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁.总之,读的时候上读锁,写的时候上写锁! 三个线程读数据,三个线程写数据示例: 可以同时读,读的时候不能写,不能同时写,写的时候不能读. 读的时候上读锁,读完解锁:写的时候上写锁,写完解锁. 注意finally解锁. package com.ljq.test.th

  • java并发编程中ReentrantLock可重入读写锁

    目录 一.ReentrantLock可重入锁 二.ReentrantReadWriteLock读写锁 三.读锁之间不互斥 一.ReentrantLock可重入锁 可重入锁ReentrantLock 是一个互斥锁,即同一时间只有一个线程能够获取锁定资源,执行锁定范围内的代码.这一点与synchronized 关键字十分相似.其基本用法代码如下: Lock lock = new ReentrantLock(); //实例化锁 //lock.lock(); //上锁 boolean locked =

  • Java中读写锁ReadWriteLock的原理与应用详解

    目录 什么是读写锁? 为什么需要读写锁? 读写锁的特点 读写锁的使用场景 读写锁的主要成员和结构图 读写锁的实现原理 读写锁总结 Java并发编程提供了读写锁,主要用于读多写少的场景,今天我就重点来讲解读写锁的底层实现原理 什么是读写锁? 读写锁并不是JAVA所特有的读写锁(Readers-Writer Lock)顾名思义是一把锁分为两部分:读锁和写锁,其中读锁允许多个线程同时获得,因为读操作本身是线程安全的,而写锁则是互斥锁,不允许多个线程同时获得写锁,并且写操作和读操作也是互斥的. 所谓的读

  • Java读写锁ReadWriteLock原理与应用场景详解

    Java并发编程提供了读写锁,主要用于读多写少的场景 什么是读写锁? 读写锁并不是JAVA所特有的读写锁(Readers-Writer Lock)顾名思义是一把锁分为两部分:读锁和写锁,其中读锁允许多个线程同时获得,因为读操作本身是线程安全的,而写锁则是互斥锁,不允许多个线程同时获得写锁,并且写操作和读操作也是互斥的. 所谓的读写锁(Readers-Writer Lock),顾名思义就是将一个锁拆分为读锁和写锁两个锁. 其中读锁允许多个线程同时获得,而写锁则是互斥锁,不允许多个线程同时获得写锁,

  • 一文搞懂Java的SPI机制(推荐)

    目录 1 简介 缺点 源码 使用 适用场景 插件扩展 案例 1 简介 SPI,Service Provider Interface,一种服务发现机制. 有了SPI,即可实现服务接口与服务实现的解耦: 服务提供者(如 springboot starter)提供出 SPI 接口.身为服务提供者,在你无法形成绝对规范强制时,适度"放权" 比较明智,适当让客户端去自定义实现 客户端(普通的 springboot 项目)即可通过本地注册的形式,将实现类注册到服务端,轻松实现可插拔 缺点 不能按需

随机推荐