写点什么

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

作者:EquatorCoco
  • 2024-04-29
    福建
  • 本文字数:3520 字

    阅读完需:约 12 分钟

一、写在开头


我们在上一篇写 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 中对于它进行了如下的描述:



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

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 类型的变量来存储溢出的悲观读锁。




四、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 了。


文章转载自:JavaBuild

原文链接:https://www.cnblogs.com/JavaBuild/p/18164898

体验地址:http://www.jnpfsoft.com/?from=infoq

用户头像

EquatorCoco

关注

还未添加个人签名 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
面试官:素有Java锁王称号的‘StampedLock’你知道吗?我:这什么鬼?_Java_EquatorCoco_InfoQ写作社区