读写锁 StampedLock

1. 前言

本节带领大家认识第二个常用的 Java 并发锁工具之 StampedLock。

本节先简单介绍 StampedLock 的基本概念,然后介绍关键的编程方法,最后通过一个编程例子为大家展示 StampedLock 工具类的用法。

下面我们正式开始介绍吧。

2. 概念解释

我们先解释一组概念:悲观锁、乐观锁。

悲观锁 指的是对数据修改持保守态度,在整个数据处理时,先将数据锁定状态,然后再进行修改处理。之所以叫做悲观锁,是因为这是一种对数据的修改抱有悲观态度的并发控制方式,认为数据被并发修改的概率比较大,所以需要在修改之前先加锁。

乐观锁 正好相反,抱着数据访问一般情况下不会造成冲突的观点,先对数据做修改,在正式提交修改结果是才会做冲突检查,如果发现修改的是旧版本的数据,则返回修改失败,否则提交修改。

StampedLock 是对 ReentrantReadWriteLock 的改进。相比 ReentrantReadWriteLock 采用了悲观锁的思想对数据修改的并发控制,StampedLock 使用了乐观思想的加锁实现,具有更高的并发性。

下面我们学习其关键的编程方法。

3. StampedLock 的编程方法

StampedLock 提供了三种并发控制模式,介绍这三种模式过程中,我们穿插介绍关键的编程方法。

3.1. 独占写模式

功能和 ReentrantReadWriteLock 的写锁类似。独占写模式相关的几个方法如下。

long stamp = writeLock () 方法:获取独占写锁,可能会被阻塞。如果获取锁成功则返回一个 stamp;
tryWriteLock () 方法:尝试获取独占写锁,类似 writeLock () 方法,只是获取不到时立刻返回不会阻塞;
tryWriteLock (long time, TimeUnit unit) 方法:允许在给定的时间内尝试获取独占写锁,超时仍然未获取到时则返回;
writeLockInterruptibly () 方法:类似 writeLock () 但允许获取锁的过程被打断;
unlockWrite (long stamp) 方法:用于释放独占写锁;
tryUnlockWrite () 方法:类似 unlockWrite (), 但允许不需要 stamp 邮戳参数。

3.2. 悲观读模式

功能和 ReentrantReadWriteLock 的读锁类似。悲观读模式相关的几个方法如下。

long stamp = readLock () 方法:获取独占读锁,可能会被阻塞。如果获取锁成功则返回一个 stamp;
unlockRead (long stamp) 方法:用于释放读锁;
tryReadLock () 方法:尝试获取读锁,类似 readLock () 方法,只是获取不到时立刻返回不会阻塞;
tryReadLock (long time, TimeUnit unit) 方法:允许在给定的时间内尝试获取读锁,超时仍然未获取到时则返回;
readLockInterruptibly () 方法:类似 readLock () 但允许获取锁的过程被打断。

3.3. 乐观读模式

这是一种优化的读模式。乐观读模式相关的几个方法如下。

tryOptimisticRead () 方法:非阻塞尝试乐观获取读锁,只有当写锁没有被获取时返回一个非 0 的 stamp 。乐观读取模式适用于短时间读取操作,降低竞争和提高吞吐量。在使用时一般需将数据存储到一个副本中,在后继处理中用于对比数据是否是最新状态;
validate (long stamp) 方法:用于检查在获取到读锁 stamp 后,锁有没被其他写线程抢占。如果写锁没有被获取,那么 validate () 方法返回 true。可多次调用验证这一信息。

另外,此类也提供了一组读写锁之间的转换方法

tryConvertToWriteLock (long stamp) 方法:尝试转换为写锁。转换条件:
tryConvertToReadLock (long stamp) 方法:尝试转换为悲观读锁。
tryConvertToOptimisticRead (long stamp) 方法:尝试转换为乐观读锁。

注意此类的编程方法有这样一个共通特征:
所有获取锁的方法,都返回一个邮戳(Stamp),Stamp 为 0 表示获取失败,其余都表示成功;
所有释放锁的方法,都需要一个邮戳(Stamp),这个 Stamp 必须是和成功获取锁时得到的 Stamp 一致;

下面我们举一个具体的编程例子。

4. 编程示例

上面介绍了核心编程方法,我们给出一个非常简洁明了的官方例子,切实体会一下 StampedLock 的用法。

import java.util.concurrent.locks.StampedLock;

public class StampedLockTest {

    // 成员变量
    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);
        }
    }

    // 一个只读方法
    // 其中存在乐观读锁到悲观读锁的转换
    double distanceFromOrigin() {

        // 尝试获取乐观读锁
        long stamp = sl.tryOptimisticRead();
        // 将全部变量拷贝到方法体栈内
        double currentX = x, currentY = y;
        // 检查在获取到读锁stamp后,锁有没被其他写线程抢占
        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) {
        long stamp = sl.tryOptimisticRead();
        try {
            // 如果当前点在原点则移动
            while (x == 0.0 && y == 0.0) {
                // 尝试将获取的读锁升级为写锁
                long ws = sl.tryConvertToWriteLock(stamp);
                // 升级成功,则更新stamp,并设置坐标值,然后退出循环
                if (ws != 0L) {
                    stamp = ws;
                    x = newX;
                    y = newY;
                    break;
                } else {
                    // 读锁升级写锁失败则释放读锁,显示获取独占写锁,然后循环重试
                    sl.unlockRead(stamp);
                    stamp = sl.writeLock();
                }
            }
        } finally {
            sl.unlock(stamp);
        }
    }
}

注意在使用时,获取锁的操作应该放在 try 之前,而释放锁的操作需要放在 finally 中,可确保锁释放。另外需要注意 StampedLock 具有不可重入性。

5. 小结

本节解释了 StampedLock 的基本概念和主要的编程方法,且通过一个简单的例子展示了其用法,更多关于此工具类的概念和原理介绍,可阅读 “Java 并发原理入门教程” 。希望大家在学习过程中,多思考勤练习,早日掌握之。