JUC工具(3):StampedLock的基础和原理

0 阅读4分钟

什么是StampedLock?

StampedLock是在Java 8中引入的一种高性能读写锁,它通过引入stamp,提供了比ReentrantReadWriteLock更加灵活、更细粒度的并发控制机制,特别适合读多写少的场景,并且它没有使用AQS

StampedLock是一种基于stamp的锁机制,支持三种访问模式:

模式说明
写锁(WriteLock)独占锁,类似 ReentrantReadWriteLock.WriteLock
悲观读锁(ReadLock)共享锁,类似 ReentrantReadWriteLock.ReadLock
乐观读(Optimistic Read)无锁读取,不阻塞写线程

它在每次加锁都会返回一个long类型的stamp,解锁的时候必须使用stamp

为什么要使用StampedLock

对于传统的读写锁ReentrantReadWriteLock,它存在两个问题:

  1. 读锁饥饿写锁:如果读线程非常多,那么会导致写线程可能长时间拿不到锁 2.读锁也是重量级锁:即便是读,也需要CAS+队列排队,仍然有开销

为了解决这些问题,StampedLock的核心思想是:能用无锁就不用加锁

尤其是引入了乐观锁,在读多写极少的情况下性能极佳。

StampedLock三种锁模式

写锁writeLock

这是一个独占锁,同一时刻只有一个线程可以获取,其他请求写锁和读锁的线程必须等待,类似于ReentrantReadWriteLock的写锁,但是一个重要的不同之处是:StampedLock的写锁是不可重入锁。

请求该锁成功后会返回一个stamp变量用来表示该锁的版本,当释放该锁时需要调用unlockWrite方法并传递获取锁时的stamp参数。并且它提供了非阻塞的tryWriteLock方法。

悲观读锁readLock

这是一个共享锁,在没有线程持有写锁的时候,多个线程可以同时持有读锁,如果已经有线程持有写锁,则其他线程请求获取该读锁会被阻塞,类似于ReentrantReadWriteLock的读锁,但是一个重要的不同之处是:StampedLock的读锁是不可重入锁。

请求该锁成功后会返回一个stamp变量用来表示该锁的版本,当释放该锁时需要调用unlockRead方法并传递stamp参数。并且它提供了非阻塞的tryReadLock方法。

乐观读锁tryOptimisticRead

相对于悲观读锁,在操作数据前并没有通过CAS设置锁的状态,仅仅通过位运算来测试。

如果当前没有线程持有写锁,则简单地返回一个非0的stamp版本信息,获取了stamp后在具体操作数据前还需要调用validate方法验证该stamp是否已经不可用。如果在这期间,有线程获取了写锁,那么validate会返回0。

乐观读锁不需要设置锁的状态,因此也不需要释放锁,效率会高很多,但也因此在保证数据一致性上需要复制一份要操作的变量到方法栈,并且操作数据的时候,可能其他写线程已经修改了数据,而我们操作的是方法栈中的数据,也就是原本数据的一个快照。

三种锁相互转换

StampedLock还支持这三种在一定条件下进行相互转换。tryConvertToWriteLock(long stamp)期望把stamp标示的锁升级为写锁,这个函数会在以下的情况下返回一个有效的stamp代表晋升写锁成功:

  1. 当前锁已经是写锁
  2. 当前锁处于读锁,并且没有其他线程持有读锁
  3. 当前锁是乐观读锁,并且当前写锁可用

image.png

StampedLock使用示例

class Point {
    // 成员变量
    private double x, y;

    // 锁实例
    private final StampedLock sl = new StampedLock();

    // 排它锁——写锁(writeLock)
    void move(double deltaX, double deltaY) {
        long stamp = sl.writeLock();
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            sl.unlockWrite(stamp);
        }
    }

    // 乐观读锁(tryOptimisticRead)
    double distanceFromOrigin() {
        // (1) 尝试获取乐观读锁
        long stamp = sl.tryOptimisticRead();

        // (2) 将全部变量复制到方法体域内
        double currentX = x, currentY = y;

        // (3) 检查在(1)处获取了读锁戳记后,锁有没有被其他写线程排他性抢占
        if (!sl.validate(stamp)) {
            // (4) 如果被抢占则获取一个共享读锁(悲观获取)
            stamp = sl.readLock();
            try {
                // (5) 将全部变量复制到方法体域内
                currentX = x;
                currentY = y;
            } finally {
                // (6) 释放共享读锁
                sl.unlockRead(stamp);
            }
        }
        // (7) 返回计算结果
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }

    // 使用悲观锁获取读锁,并尝试转换为写锁
    void moveIfAtOrigin(double newX, double newY) {
        // (1) 这里可以使用乐观读锁替换
        long stamp = sl.readLock();
        try {
            // (2) 如果当前点在原点则移动
            while (x == 0.0 && y == 0.0) {
                // (3) 尝试将获取的读锁升级为写锁
                long ws = sl.tryConvertToWriteLock(stamp);

                // (4) 升级成功,则更新戳记,并设置坐标值,然后退出循环
                if (ws != 0L) {
                    stamp = ws;
                    x = newX;
                    y = newY;
                    break;
                } else {
                    // (5) 读锁升级写锁失败则释放读锁,显式获取独占写锁,然后循环重试
                    sl.unlockRead(stamp);
                    stamp = sl.writeLock();
                }
            }
        } finally {
            // (6) 释放锁
            sl.unlock(stamp);
        }
    }
}