读写锁允许多个读者并行,但每次获取读锁仍需维护锁状态。Java 8 的 StampedLock 增加了乐观读:先尝试读取,之后检查期间有没有写操作。

三种访问模式

writeLock 是独占写锁,readLock 是悲观读锁。二者取得的 stamp 用于后续解锁。

tryOptimisticRead() 返回一个用于校验的标记(stamp),并不会真正加上读锁。写线程仍然可以修改数据,所以读取后需要调用 validate(stamp),检查读取期间是否发生了写操作。

import java.util.concurrent.locks.StampedLock;

public class StampedPointDemo {
    private final StampedLock lock = new StampedLock();
    private double x;
    private double y;

    public void move(double nextX, double nextY) {
        long stamp = lock.writeLock();
        try {
            x = nextX;
            y = nextY;
        } finally {
            lock.unlockWrite(stamp);
        }
    }

    public double distance() {
        long stamp = lock.tryOptimisticRead();
        double currentX = x;
        double currentY = y;
        if (!lock.validate(stamp)) {
            stamp = lock.readLock();
            try {
                currentX = x;
                currentY = y;
            } finally {
                lock.unlockRead(stamp);
            }
        }
        return Math.hypot(currentX, currentY);
    }

    public static void main(String[] args) throws InterruptedException {
        StampedPointDemo point = new StampedPointDemo();
        Thread writer = new Thread(() -> point.move(3, 4));
        writer.start();
        writer.join();
        System.out.println(point.distance());
    }
}

输出 5.0。乐观读取失败后,先取得真正的读锁,再重新读两个字段,不能继续使用前面那组可能不一致的值。

先读到局部变量,再校验

validate 要放在读取之后。先 validate 成功,再从共享对象读取,写者仍可能在这中间进入,前一次校验就不能保护后面的读取。

乐观读取期间得到的字段可能暂时不一致。因此在校验前,不要依据它们做有副作用的操作,也不要随意遍历会并发改变结构的集合。像上面的简单数值快照容易处理,复杂对象图可能直接使用悲观读锁更稳妥。

如果 tryOptimisticRead 返回 0,说明当时有写锁持有者,validate 会失败并进入读锁路径。

锁转换

tryConvertToWriteLock(stamp) 可以尝试转换为写锁,成功返回新的非零 stamp,失败返回 0。只有满足相应条件,例如自己是唯一读者时,读锁才有机会直接转换成功。

转换失败后,原来的读锁并没有因为这次失败自动释放。需要释放原读锁,再获取写锁,并重新检查业务条件。解锁时必须使用当前有效的 stamp和对应模式,不能一直沿用最初的值。

与 ReentrantReadWriteLock 的区别

StampedLock 不支持重入。已经取得写锁,再调用一个也要获取写锁的方法,可能把自己阻塞住。它也没有通常意义上的线程所有权,使用时仍要保存并正确传递 stamp。

它不提供 Condition。需要等待锁时响应中断,可以选择 readLockInterruptibly()、writeLockInterruptibly(),不要把普通 readLock/writeLock 当成可中断获取。

乐观读减少了读者对共享锁状态的更新,但失败时需要重读甚至获取读锁。写入频繁或读取逻辑复杂时,不一定有优势,适合不适合仍然取决于访问模式。

最后修改:2018 年 05 月 31 日
如果觉得我的文章对你有用,请随意赞赏