首页 > 其他分享 >StampedLock:JDK1.8中新增,比ReadWriteLock还快的锁

StampedLock:JDK1.8中新增,比ReadWriteLock还快的锁

时间:2023-04-07 15:32:20浏览次数:48  
标签:JDK1.8 stamp ReadWriteLock StampedLock 写锁 读锁 线程 sl

摘要:StampedLock是一种在读取共享变量的过程中,允许后面的一个线程获取写锁对共享变量进行写操作,使用乐观读避免数据不一致的问题,并且在读多写少的高并发环境下,比ReadWriteLock更快的一种锁。

本文分享自华为云社区《一文彻底理解并发编程中非常重要的票据锁——StampedLock》,作者:冰 河 。

什么是StampedLock?

ReadWriteLock锁允许多个线程同时读取共享变量,但是在读取共享变量的时候,不允许另外的线程多共享变量进行写操作,更多的适合于读多写少的环境中。那么,在读多写少的环境中,有没有一种比ReadWriteLock更快的锁呢?

答案当然是有!那就是我们今天要介绍的主角——JDK1.8中新增的StampedLock!没错,就是它!

StampedLock与ReadWriteLock相比,在读的过程中也允许后面的一个线程获取写锁对共享变量进行写操作,为了避免读取的数据不一致,使用StampedLock读取共享变量时,需要对共享变量进行是否有写入的检验操作,并且这种读是一种乐观读。

总之,StampedLock是一种在读取共享变量的过程中,允许后面的一个线程获取写锁对共享变量进行写操作,使用乐观读避免数据不一致的问题,并且在读多写少的高并发环境下,比ReadWriteLock更快的一种锁。

StampedLock三种锁模式

这里,我们可以简单对比下StampedLock与ReadWriteLock,ReadWriteLock支持两种锁模式:一种是读锁,另一种是写锁,并且ReadWriteLock允许多个线程同时读共享变量,在读时,不允许写,在写时,不允许读,读和写是互斥的,所以,ReadWriteLock中的读锁,更多的是指悲观读锁。

StampedLock支持三种锁模式:写锁、读锁(这里的读锁指的是悲观读锁)和乐观读(很多资料和书籍写的是乐观读锁,这里我个人觉得更准确的是乐观读,为啥呢?我们继续往下看啊)。其中,写锁和读锁与ReadWriteLock中的语义类似,允许多个线程同时获取读锁,但是只允许一个线程获取写锁,写锁和读锁也是互斥的。

另一个与ReadWriteLock不同的地方在于:StampedLock在获取读锁或者写锁成功后,都会返回一个Long类型的变量,之后在释放锁时,需要传入这个Long类型的变量。例如,下面的伪代码所示的逻辑演示了StampedLock如何获取锁和释放锁。

public class StampedLockDemo{
 //创建StampedLock锁对象
 public StampedLock stampedLock = new StampedLock();
 //获取、释放读锁
 public void testGetAndReleaseReadLock(){
 long stamp = stampedLock.readLock();
 try{
 //执行获取读锁后的业务逻辑
 }finally{
 //释放锁
 stampedLock.unlockRead(stamp);
 }
 }
 //获取、释放写锁
 public void testGetAndReleaseWriteLock(){
 long stamp = stampedLock.writeLock();
 try{
 //执行获取写锁后的业务逻辑。
 }finally{
 //释放锁
 stampedLock.unlockWrite(stamp);
 }
 }
}

StampedLock支持乐观读,这是它比ReadWriteLock性能要好的关键所在。 ReadWriteLock在读取共享变量时,所有对共享变量的写操作都会被阻塞。而StampedLock提供的乐观读,在多个线程读取共享变量时,允许一个线程对共享变量进行写操作。

我们再来看一下JDK官方给出的StampedLock示例,如下所示。

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();
 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);
 }
 }
}

在上述代码中,如果在执行乐观读操作时,另外的线程对共享变量进行了写操作,则会把乐观读升级为悲观读锁,如下代码片段所示。

double distanceFromOrigin() { // A read-only method
 //乐观读
 long stamp = sl.tryOptimisticRead();
 double currentX = x, currentY = y;
 //判断是否有线程对变量进行了写操作
 //如果有线程对共享变量进行了写操作
 //则sl.validate(stamp)会返回false
 if (!sl.validate(stamp)) {
 //将乐观读升级为悲观读锁
        stamp = sl.readLock();
 try {
 currentX = x;
 currentY = y;
 } finally {
 //释放悲观锁
 sl.unlockRead(stamp);
 }
 }
 return Math.sqrt(currentX * currentX + currentY * currentY);
}

这种将乐观读升级为悲观读锁的方式相比一直使用乐观读的方式更加合理,如果不升级为悲观读锁,则程序会在一个循环中反复执行乐观读操作,直到乐观读操作期间没有线程执行写操作,而在循环中不断的执行乐观读会消耗大量的CPU资源,升级为悲观读锁是更加合理的一种方式。

StampedLock实现思想

StampedLock内部是基于CLH锁实现的,CLH是一种自旋锁,能够保证没有“饥饿现象”的发生,并且能够保证FIFO(先进先出)的服务顺序。

在CLH中,锁维护一个等待线程队列,所有申请锁,但是没有成功的线程都会存入这个队列中,每一个节点代表一个线程,保存一个标记位(locked),用于判断当前线程是否已经释放锁,当locked标记位为true时, 表示获取到锁,当locked标记位为false时,表示成功释放了锁。

当一个线程试图获得锁时,取得等待队列的尾部节点作为其前序节点,并使用类似如下代码判断前序节点是否已经成功释放锁:

while (pred.locked) {
 //省略操作 
}

只要前序节点(pred)没有释放锁,则表示当前线程还不能继续执行,因此会自旋等待;反之,如果前序线程已经释放锁,则当前线程可以继续执行。

释放锁时,也遵循这个逻辑,线程会将自身节点的locked位置标记为false,后续等待的线程就能继续执行了,也就是已经释放了锁。

StampedLock的实现思想总体来说,还是比较简单的,这里就不展开讲了。

StampedLock的注意事项

在读多写少的高并发环境下,StampedLock的性能确实不错,但是它不能够完全取代ReadWriteLock。在使用的时候,也需要特别注意以下几个方面。

StampedLock不支持重入

没错,StampedLock是不支持重入的,也就是说,在使用StampedLock时,不能嵌套使用,这点在使用时要特别注意。

StampedLock不支持条件变量

第二个需要注意的是就是StampedLock不支持条件变量,无论是读锁还是写锁,都不支持条件变量。

StampedLock使用不当会导致CPU飙升

这点也是最重要的一点,在使用时需要特别注意:如果某个线程阻塞在StampedLock的readLock()或者writeLock()方法上时,此时调用阻塞线程的interrupt()方法中断线程,会导致CPU飙升到100%。例如,下面的代码所示。

public void testStampedLock() throws Exception{
 final StampedLock lock = new StampedLock();
 Thread thread01 = new Thread(()->{
 // 获取写锁
 lock.writeLock();
 // 永远阻塞在此处,不释放写锁
 LockSupport.park();
 });
    thread01.start();
 // 保证thread01获取写锁
 Thread.sleep(100);
 Thread thread02 = new Thread(()->
 //阻塞在悲观读锁
 lock.readLock()
 );
    thread02.start();
 // 保证T2阻塞在读锁
 Thread.sleep(100);
 //中断线程thread02
 //会导致线程thread02所在CPU飙升
    thread02.interrupt();
    thread02.join();
}

运行上面的程序,会导致thread02线程所在的CPU飙升到100%。

这里,有很多小伙伴不太明白为啥LockSupport.park();会导致thread01会永远阻塞。这里,冰河为你画了一张线程的生命周期图,如下所示。

StampedLock:JDK1.8中新增,比ReadWriteLock还快的锁_JDK1.8

这下明白了吧?在线程的生命周期中,有几个重要的状态需要说明一下。

  • NEW:初始状态,线程被构建,但是还没有调用start()方法。
  • RUNNABLE:可运行状态,可运行状态可以包括:运行中状态和就绪状态。
  • BLOCKED:阻塞状态,处于这个状态的线程需要等待其他线程释放锁或者等待进入synchronized。
  • WAITING:表示等待状态,处于该状态的线程需要等待其他线程对其进行通知或中断等操作,进而进入下一个状态。
  • TIME_WAITING:超时等待状态。可以在一定的时间自行返回。
  • TERMINATED:终止状态,当前线程执行完毕。

看完这个线程的生命周期图,知道为啥调用LockSupport.park();会使thread02阻塞了吧?

所以,在使用StampedLock时,一定要注意避免线程所在的CPU飙升的问题。那如何避免呢?

那就是使用StampedLock的readLock()方法或者读锁和使用writeLock()方法获取写锁时,一定不要调用线程的中断方法来中断线程,如果不可避免的要中断线程的话,一定要用StampedLock的readLockInterruptibly()方法获取可中断的读锁和使用StampedLock的writeLockInterruptibly()方法获取可中断的悲观写锁。

最后,对于StampedLock的使用,JDK官方给出的StampedLock示例本身就是一个最佳实践了,小伙伴们可以多看看JDK官方给出的StampedLock示例,多多体会下StampedLock的使用方式和背后原理与核心思想。


点击关注,第一时间了解华为云新鲜技术~

标签:JDK1.8,stamp,ReadWriteLock,StampedLock,写锁,读锁,线程,sl
From: https://blog.51cto.com/u_15214399/6176342

相关文章

  • JDK1.8新特性
    JDK1.8新特性1.Java81.1生态Lambda表达式函数式接口方法引用/构造器引用StreamAPI接口中的默认方法/静态方法新时间日期API其他新特性1.2新特性......
  • Linux安装JDK1.8并配置环境变量
    @目录一、下载JDK安装包二、安装JDK三、配置环境变量四、验证JDK是否安装成功Hello,大家好!今天分享Linux安装配置JDK1.8教程,本教程是学长在项目上线时撰写的,经过生产环境......
  • HashMap的put()方法解读(Jdk1.8)
    通过本章内容来给大家解读hashMap中put()方法的逻辑流程。目录:-put()方法流程图-put()方法代码解读- 单元测试代码- put方法-......
  • (linux)CentOS -yum 安装jdk1.8
    1、搜索jdk安装:yumsearchjava|grepjdk12、安装jdk1.8:yuminstalljava-1.8.0-openjdk查看是否安装成功:java-version3、环境变量配置:JDK`默认安装路径`/usr/lib/jvm......
  • HashMap在Jdk1.7和Jdk1.8中的区别
    底层数据结构不同。在Jdk1.7中,HashMap是基于数组+链表的形式实现。而在Jdk1.8中,是由数组+链表+红黑树的形式实现。hashcode计算的复杂度不同。Jdk1.7中的hashCode计算大......
  • JDK1.8 新特性
    3.1Lambda表达式3.1.1概念​Lambda是一个匿名函数,可以理解为一段可以传递的代码(将代码像数据一样传递);可以写出更简洁、更灵活的代码;作为一种更紧凑的代码风格,是......
  • windows下载安装jdk1.8(jdk8)基础篇
    ​  一、前言目前jdk最高升级到JDK19版本了,但是大部分应用系统都是用的1.8,对于初学者来说,也需要下载安装这个版本的jdk。二、下载安装步骤一、我已经下载下来,大家到......
  • Java的Lambda表达式总结-JDK1.8
    前言:lambda是一个匿名函数()里的表示参数{}里的表示方法体->表示lambda运算符 常用的表达式如下: 一、groupingBy用法目的:数据库(想把相同的合......
  • JDK1.8新特性
    JDK1.8新特性1、Lambda表达式2、::运算符引用方法3、streamStream是JDK1.8新增的一个特性,称之为Stream,它可以给予Lamdba表达式对集合数据进行处理(如:遍历、排序、去重......
  • linux系统安装jdk1.8、mysql5.7、redis(压缩包版本教程)
    linux系统安装jdk1.8、mysql5.7、redis(压缩包版本教程)在内网环境中的linux系统上安装环境,十分的麻烦(没有网络)一、安装jdk1.81.在use/local下创建相关文件夹:1.mkdirja......