面试官:素有Java锁王称号的‘StampedLock’你知道吗?我:这什么鬼?

java,stampedlock · 浏览次数 : 2

小编点评

**StampedLock** 是一个用于高性能读写锁的 Java 接口,它比传统锁具有以下特性: * **乐观读:**允许一个写线程获取写锁,而不会阻塞所有写线程。 * **不可重入:**不支持条件变量 Condition,对中断操作不友好。 * **性能优越:**比传统锁具有更高的性能。 **使用 StampedLock 的优点:** * **提高并发性:**允许多个线程在读写锁之间并发执行。 * **减少线程饥饿:**写锁不会阻塞所有写线程,减少线程饥饿问题。 * **提高性能:**通过使用乐观读来减少锁冲突的次数。 **StampedLock 的使用注意事项:** * **不可重入:**使用 StampedLock 的过程中,线程必须始终保持一致的锁状态。 * **条件变量 Condition:**使用 StampedLock 的过程中,条件变量 Condition 无法参与。 * **中断操作:**使用 StampedLock 的过程中,中断操作可能导致 CPU 飙升。 **总结:** StampedLock 是一种性能优异的 Java 接口,可以用于处理高并发性读写锁的需求。然而,由于其不可重入特性和条件变量 Condition 限制,该接口应该仅在需要性能优化的场景中使用。

正文

一、写在开头


我们在上一篇写ReentrantReadWriteLock读写锁的末尾留了一个小坑,那就是读写锁因为写锁的悲观性,会导致 “写饥饿”,这样一来会大大的降低读写效率,而今天我们就来将此坑填之!填坑工具为:StampedLock,一个素有Java锁王称号的同步类,也是在 java.util.concurrent.locks 包中。

需要声明的是,这个类在Java的面试过程中极少被问及,如果仅仅是为了准备面试的话,这部分内容可以忽略,但这个类的实现逻辑还是值得一学的。


二、StampedLock 是什么?


StampedLock是由Java8时引入的一个性能更好的读写锁,作者:Doug Lea,支持读锁、写锁,这与ReentrantReadWriteLock类似,但同时多了一个乐观读锁的实现,这一点直接提升了它的性能。


三、StampedLock的原理


虽然StampedLock性能更好,但是!不可重入且不支持条件变量 Condition,且并没有直接实现Lock或者ReadWriteLock接口,而是与AQS类似的采用CLH(Craig, Landin, and Hagersten locks)作为底层实现。

在Java的官方docs中对于它进行了如下的描述:

image

并且官方还提供了一个示例,我们来看一下:

class Point {
	//共享变量
   private double x, y;
   private final StampedLock sl = new StampedLock();

   // 写锁的使用
   void move(double deltaX, double deltaY) {
     long stamp = sl.writeLock(); //涉及对共享资源的修改,使用写锁-独占操作
     try {
       x += deltaX;
       y += deltaY;
     } finally {
       sl.unlockWrite(stamp); // 释放写锁
     }
   }

  	/**
     * 使用乐观读锁访问共享资源
     * 注意:乐观读锁在保证数据一致性上需要拷贝一份要操作的变量到方法栈,并且在操作数据时候					可能其他写线程已经修改了数据,
     * 而我们操作的是方法栈里面的数据,也就是一个快照,所以最多返回的不是最新的数据,但是一致性还是得到保障的。
     *
     * @return
     */
   double distanceFromOrigin() {
     long stamp = sl.tryOptimisticRead(); // 获取乐观读锁
     double currentX = x, currentY = y;	// 拷贝共享资源到本地方法栈中
     if (!sl.validate(stamp)) { // //检查乐观读锁后是否有其他写锁发生,有则返回false
        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) {
     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); // 释放所有锁
     }
   }
}

在StampedLock 的底层提供了三种锁

  1. 写锁: 独占锁,一把锁只能被一个线程获得。当一个线程获取写锁后,其他请求读锁和写锁的线程必须等待。类似于 ReentrantReadWriteLock 的写锁,不过这里的写锁是不可重入的。
  2. 读锁 (悲观读):共享锁,没有线程获取写锁的情况下,多个线程可以同时持有读锁。如果己经有线程持有写锁,则其他线程请求获取该读锁会被阻塞。类似于 ReentrantReadWriteLock 的读锁,不过这里的读锁是不可重入的。
  3. 乐观读 :允许多个线程获取乐观读以及读锁。同时允许一个写线程获取写锁。

【源码示例】

// 写锁
public long writeLock() {
    long s, next;  // bypass acquireWrite in fully unlocked case only
    return ((((s = state) & ABITS) == 0L &&
             U.compareAndSwapLong(this, STATE, s, next = s + WBIT)) ?
            next : acquireWrite(false, 0L));
}
// 读锁
public long readLock() {
    long s = state, next;  // bypass acquireRead on common uncontended case
    return ((whead == wtail && (s & ABITS) < RFULL &&
             U.compareAndSwapLong(this, STATE, s, next = s + RUNIT)) ?
            next : acquireRead(false, 0L));
}
// 乐观读
public long tryOptimisticRead() {
    long s;
    return (((s = state) & WBIT) == 0L) ? (s & SBITS) : 0L;
}

StampedLock 在获取锁的时候会返回一个 long 型的数据戳,该数据戳用于稍后的锁释放参数,如果返回的数据戳为 0 则表示锁获取失败。当前线程持有了锁再次获取锁还是会返回一个新的数据戳,这也是StampedLock不可重入的原因。此外,在官网给的示例中我们也看到了,StampedLock 还支持这3种锁的转换:

long tryConvertToWriteLock(long stamp){}
long tryConvertToReadLock(long stamp){}
long tryConvertToOptimisticRead(long stamp){}

内部常量说明

在源码中我们看到,无论哪种锁,在获取的时候都会返回一个long类型的时间戳,这其实就是StampedLock命名的由来,而这个时间戳的第8位用来标识写锁,前 7 位(LG_READERS)来表示读锁,每获取一个悲观读锁,就加 1(RUNIT),每释放一个悲观读锁,就减 1。而悲观读锁最多只能装 128 个(7 位限制),很容易溢出,所以用一个 int 类型的变量来存储溢出的悲观读锁。

image


四、StampedLock的使用


结果上面的StampedLock特性和官方的示例,我们写一个小demo来感受一下它的使用,需要注意的是在获取乐观锁时,如果有写锁改变数据时,为保证数据一致性,要切换为普通的读锁模式。

【测试示例】

public class Test {

    private final StampedLock sl = new StampedLock();
    private int data = 0;

    public void write(int value) {
        long stamp = sl.writeLock();
        try {
            data = value;
        } finally {
            sl.unlockWrite(stamp);
        }
    }

    public int read() {
        long stamp = sl.tryOptimisticRead();
        int currentData = data;
        // 如果有写锁被占用,可能造成数据不一致,所以要切换到普通读锁模式
        if (!sl.validate(stamp)) {
            stamp = sl.readLock();
            try {
                currentData = data;
            } finally {
                sl.unlockRead(stamp);
            }
        }
        return currentData;
    }

    public static void main(String[] args) {
        Test test = new Test();

        Thread writer = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                test.write(i);
                System.out.println("当前线程" + Thread.currentThread().getName() + ":Write: " + i);
            }
        });

        Thread reader = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                int value = test.read();
                System.out.println("当前线程" + Thread.currentThread().getName() + ":Read: " + value);
            }
        });

        writer.start();
        reader.start();
    }
}

输出:

当前线程Thread-0:Write: 0
当前线程Thread-0:Write: 1
当前线程Thread-1:Read: 0
当前线程Thread-0:Write: 2
当前线程Thread-1:Read: 2
当前线程Thread-0:Write: 3
当前线程Thread-1:Read: 3
当前线程Thread-0:Write: 4
当前线程Thread-1:Read: 4
当前线程Thread-1:Read: 4

五、总结

相比于传统读写锁多出来的乐观读是StampedLock比 ReadWriteLock 性能更好的关键原因。StampedLock 的乐观读允许一个写线程获取写锁,所以不会导致所有写线程阻塞,也就是当读多写少的时候,写线程有机会获取写锁,减少了线程饥饿的问题,吞吐量大大提高。

不过,需要注意的是StampedLock不可重入,不支持条件变量 Condition,对中断操作支持也不友好(使用不当容易导致 CPU 飙升)。如果你需要用到 ReentrantLock 的一些高级性能,就不太建议使用 StampedLock 了。

六、结尾彩蛋

如果本篇博客对您有一定的帮助,大家记得留言+点赞+收藏呀。原创不易,转载请联系Build哥!

image

如果您想与Build哥的关系更近一步,还可以关注“JavaBuild888”,在这里除了看到《Java成长计划》系列博文,还有提升工作效率的小笔记、读书心得、大厂面经、人生感悟等等,欢迎您的加入!
image

与面试官:素有Java锁王称号的‘StampedLock’你知道吗?我:这什么鬼?相似的内容:

面试官:素有Java锁王称号的‘StampedLock’你知道吗?我:这什么鬼?

一、写在开头 我们在上一篇写ReentrantReadWriteLock读写锁的末尾留了一个小坑,那就是读写锁因为写锁的悲观性,会导致 “写饥饿”,这样一来会大大的降低读写效率,而今天我们就来将此坑填之!填坑工具为:StampedLock,一个素有Java锁王称号的同步类,也是在 java.util

Java中的读写锁ReentrantReadWriteLock详解,存在一个小缺陷

写在开头 最近是和java.util.concurrent.locks包下的同步类干上了,素有 并发根基 之称的concurrent包中全是精品,今天我们继续哈,今天学习的主题要由一个大厂常问的Java面试题开始: 小伙子,来说一说Java中的读写锁,你都用过哪些读写锁吧? 这个问题小伙伴们遇到了该

AI 一键生成高清短视频,视频 UP 主们卷起来...

现在短视频越来越火,据统计,2023年全球短视频用户数量已达 10 亿,预计到2027年将突破 24 亿。对于产品展示和用户营销来说,短视频已经成为重要阵地,不管你喜不喜欢它,你都得面对它,学会使用它。 但是,优质短视频的持续输出对视频创作者来说挑战还是很大的。选题策划、文案撰写、素材收集、剪辑制作

面试官:Dubbo一次RPC请求经历哪些环节?

大家好,我是三友~~ 今天继续探秘系列,扒一扒一次RPC请求在Dubbo中经历的核心流程。 本文是基于Dubbo3.x版本进行讲解 一个简单的Demo 这里还是老样子,为了保证文章的完整性和连贯性,方便那些没有使用过的小伙伴更加容易接受文章的内容,这里快速讲一讲Dubbo一个简单的Demo 如果你已

面试官:Java线程可以无限创建吗?

哈喽,大家好,我是世杰。 ⏩本次给大家介绍一下操作系统线程和Java的线程以及二者的关联 1. 面试连环call Java线程可以无限创建吗? Java线程和操作系统线程有什么关联? 操作系统为什么要区分内核态和用户态? ⏩要想解答这些问题,我们要先从操作系统线程开始说起,让我们开始吧�

面试官:如何打破双亲委派机制?

面试连环call:1. 双亲委派机制是什么?如何打破双亲委派机制?2. JVM都有哪些类加载器?3. 如何构造一个自定义类加载器?

面试官:Java类是如何被加载到内存中的?

面试连环call Java类是如何被加载到内存中的? Java类的生命周期都有哪些阶段? JVM加载的class文件都有哪些来源? JVM在加载class文件时,何时判断class文件的格式是否符合要求? 类生命周期 一个类从被加载到虚拟机内存开始,到卸载出内存为止,它的整个生命周期将会经历加载、验

面试官:JVM调优,主要针对是哪一个区域?JVM内存结构是怎样的?

作为一个Java程序员,在日常的开发中,不必像C/C++程序员那样,为每一个内存的分配而操心,JVM会替我们进行自动的内存分配和回收,方便我们开发。但是一旦发生内存泄漏或者内存溢出,如果对Java内存结构不清楚,那将会是一件非常麻烦的事情!本文笔者将为大家详解Java内存结构。

面试官:你了解git cherry-pick吗?

事情要从一次不规范的代码开发开始说起 背景故事 时间 2024年某个风平浪静的周五晚上 地点 中国,北京,西二旗,某互联网大厂会议室 人物 小杰,小A,小B,老K 对话 老K:昨天提交的代码被测试打回来了!为什么小B没开发完的内容也一起提交上去了? 小B:啊?我不清楚啊,我在开发分支B开发完一部分就

面试官:transient关键字修饰的变量当真不可序列化?我:烦请先生教我!

一、写在开头 在这篇文章中记录一下之前自己面试时学到的东西,是关于transient关键字的,当时面试官问我IO的相关问题,基本上全答出来了,关于如何不序列化对象中某个字段时,我果断的选择了static和transient,但面试官紧接着问了我:“transient关键字修饰的变量当真不可序列化吗?