StampedLock是什么

StampedLock 是 JDK 8 引入的一种新型锁机制,位于 java.util.concurrent.locks 包下。它是对读写锁(ReentrantReadWriteLock)的优化,提供了三种访问模式:

  1. 写锁(Write Lock):独占锁,类似 ReentrantReadWriteLock 的写锁。
  2. 读锁(Read Lock):共享锁,类似 ReentrantReadWriteLock 的读锁。
  3. 乐观读(Optimistic Read)无锁机制,通过“戳记(Stamp)”验证数据是否被修改,避免完全加读锁的开销。

相比传统的 ReentrantReadWriteLock,StampedLock 在某些场景下能提供更好的性能。

特点

  1. 不可重入:StampedLock 的锁不可重入,同一个线程重复获取锁会导致死锁
  2. 不支持条件变量:不像 ReentrantLock 那样支持 Condition
  3. 票据(Stamp)机制:所有锁方法都返回一个 stamp,用于后续的解锁或验证操作

核心方法

1、写锁操作

// 获取写锁,返回 stamp
long writeStamp = lock.writeLock();
try {
    // 写操作
} finally {
    // 释放写锁,需要传入正确的 stamp
    lock.unlockWrite(writeStamp);
}

2、悲观读锁操作

// 获取读锁,返回 stamp
long readStamp = lock.readLock();
try {
    // 读操作
} finally {
    // 释放读锁,需要传入正确的 stamp
    lock.unlockRead(readStamp);
}

3、乐观读操作

// 尝试乐观读,返回 stamp
long stamp = lock.tryOptimisticRead();
// 读取共享变量
int value = sharedValue;
// 验证 stamp 是否仍然有效(期间没有写操作)
if (!lock.validate(stamp)) {
    // 如果无效,升级为悲观读锁
    stamp = lock.readLock();
    try {
        value = sharedValue;
    } finally {
        lock.unlockRead(stamp);
    }
}
// 使用读取的值

为什么 JDK 8 还要推出 StampedLock?

虽然已有 synchronizedReentrantLockReentrantReadWriteLock,但它们在高并发读多写少的场景下仍存在性能瓶颈。StampedLock 的出现是为了解决以下问题:

1、读写锁(ReentrantReadWriteLock)的缺陷

  • 读锁与写锁互斥:读锁会阻塞写锁,写锁会阻塞所有读锁,即使读操作非常频繁,写操作也可能长时间饥饿。
  • 无乐观读机制:读锁必须加锁,即使数据未被修改,也会带来不必要的同步开销。

2、StampedLock 的优势

  • 乐观读:通过 tryOptimisticRead() 获取一个“戳记”(Stamp),读数据时不加锁,后续通过 validate(stamp) 检查数据是否被修改。如果未被修改,直接使用;如果被修改,再升级为悲观读锁。

    StampedLock lock = new StampedLock();
    long stamp = lock.tryOptimisticRead(); // 乐观读(无锁)
    // 读取共享数据...
    if (!lock.validate(stamp)) { // 检查是否被修改
        stamp = lock.readLock(); // 升级为悲观读锁
        try {
            // 重新读取数据...
        } finally {
            lock.unlockRead(stamp);
        }
    }
    
  • 更高的吞吐量:乐观读避免了读锁的竞争,特别适合读多写少的场景。

  • 支持锁升级/降级:例如,可以将乐观读升级为悲观读锁或写锁。

3、对比其他锁的性能

  • 读远多于写的场景下,StampedLock 的吞吐量显著高于 ReentrantReadWriteLock
  • StampedLock 不是可重入锁,且没有条件变量(Condition)支持,复杂场景需谨慎使用。

适用场景

  • 适合读多写少的高并发场景(如缓存、监控统计)。
  • 不适用:
    • 需要重入锁或条件变量的场景(用 ReentrantLock)。
    • 写操作频繁的场景(可能让乐观读失效频繁,退化为悲观锁)。

使用案例

简单的读写操作

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); // 释放写锁
        }
    }

    double distanceFromOrigin() {
        long stamp = sl.tryOptimisticRead(); // 尝试乐观读
        double currentX = x, currentY = y;
        if (!sl.validate(stamp)) { // 检查是否有写操作发生
            stamp = sl.readLock(); // 获取悲观读锁
            try {
                currentX = x;
                currentY = y;
            } finally {
                sl.unlockRead(stamp); // 释放读锁
            }
        }
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }
}
  • writeLock()获取独占写锁,返回一个”戳记”(stamp)用于后续解锁
  • tryOptimisticRead()尝试乐观读,不阻塞其他线程的写操作,返回一个戳记
  • validate(stamp)检查自获取戳记以来是否有写操作发生:
    • 如果没有(返回true),说明读取的值是有效的,可以直接使用
    • 如果有(返回false),则需要获取悲观读锁重新读取
  • 如果验证失败,获取真正的读锁(readLock()),然后重新读取值

锁转换

展示 StampedLock 的一个高级特性:读锁转换为写锁

class CachedData {
    private Object data;
    private final StampedLock lock = new StampedLock();

    void processCachedData() {
        long stamp = lock.readLock();
        try {
            while (!isValid(data)) {
                long ws = lock.tryConvertToWriteLock(stamp);
                if (ws != 0L) {
                    stamp = ws;
                    data = refreshData(); // 更新缓存
                    break;
                } else {
                    lock.unlockRead(stamp);
                    stamp = lock.writeLock();
                }
            }
            use(data);
        } finally {
            lock.unlock(stamp);
        }
    }

    private boolean isValid(Object data) { /*...*/ }
    private Object refreshData() { /*...*/ }
    private void use(Object data) { /*...*/ }
}
  • tryConvertToWriteLock():尝试将现有的读锁转换为写锁,而无需先释放读锁再获取写锁,这种操作称为锁转换锁升级
    • 如果转换成功,返回一个有效的写锁戳记(非零)
    • 如果转换失败,返回0

与ReentrantReadWriteLock对比

场景描述高并发读写缓存

假设有一个全局配置缓存 configCache,多个线程频繁读取配置,偶尔有线程更新配置。

  • 读操作频率:每秒 10,000+ 次
  • 写操作频率:每秒 1~2 次

1、使用 ReentrantReadWriteLock 的实现

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ConfigCacheWithReadWriteLock {
    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    private String configData = "default";

    public String readConfig() {
        rwLock.readLock().lock(); // 加读锁(阻塞写锁)
        try {
            return configData;
        } finally {
            rwLock.readLock().unlock();
        }
    }

    public void updateConfig(String newData) {
        rwLock.writeLock().lock(); // 加写锁(阻塞所有读锁)
        try {
            configData = newData;
        } finally {
            rwLock.writeLock().unlock();
        }
    }
}

2、使用 StampedLock 的实现

import java.util.concurrent.locks.StampedLock;

public class ConfigCacheWithStampedLock {
    private final StampedLock stampedLock = new StampedLock();
    private String configData = "default";

    // 乐观读:无锁尝试
    public String readConfig() {
        long stamp = stampedLock.tryOptimisticRead();
        String currentData = configData; // 读取数据(无锁)
        if (!stampedLock.validate(stamp)) { // 检查是否被修改
            stamp = stampedLock.readLock(); // 升级为悲观读锁
            try {
                currentData = configData; // 重新读取
            } finally {
                stampedLock.unlockRead(stamp);
            }
        }
        return currentData;
    }

    // 写操作:独占锁
    public void updateConfig(String newData) {
        long stamp = stampedLock.writeLock();
        try {
            configData = newData;
        } finally {
            stampedLock.unlockWrite(stamp);
        }
    }
}

使用 JMH 进行性能测试

导入坐标:

<dependencies>
        <!-- JMH 依赖 -->
        <dependency>
            <groupId>org.openjdk.jmh</groupId>
            <artifactId>jmh-core</artifactId>
            <version>1.36</version>
        </dependency>
        <dependency>
            <groupId>org.openjdk.jmh</groupId>
            <artifactId>jmh-generator-annprocess</artifactId>
            <version>1.36</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.2.4</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <finalName>benchmarks</finalName>
                            <transformers>
                                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <mainClass>org.openjdk.jmh.Main</mainClass>
                                </transformer>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

JMH 测试类

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.concurrent.TimeUnit;

@BenchmarkMode(Mode.Throughput) // 测试吞吐量
@OutputTimeUnit(TimeUnit.MILLISECONDS) // 输出单位:毫秒
@State(Scope.Benchmark)
public class LockBenchmark {

    private ConfigCacheWithReadWriteLock readWriteLockCache;
    private ConfigCacheWithStampedLock stampedLockCache;

    @Setup
    public void setup() {
        readWriteLockCache = new ConfigCacheWithReadWriteLock();
        stampedLockCache = new ConfigCacheWithStampedLock();
    }

    // 测试 ReentrantReadWriteLock 的读性能
    @Benchmark
    public String testReadWriteLockRead() {
        return readWriteLockCache.readConfig();
    }

    // 测试 StampedLock 的读性能
    @Benchmark
    public String testStampedLockRead() {
        return stampedLockCache.readConfig();
    }

    // 测试 ReentrantReadWriteLock 的写性能
    @Benchmark
    public void testReadWriteLockWrite() {
        readWriteLockCache.updateConfig("newValue");
    }

    // 测试 StampedLock 的写性能
    @Benchmark
    public void testStampedLockWrite() {
        stampedLockCache.updateConfig("newValue");
    }

    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(LockBenchmark.class.getSimpleName())
                .forks(1) // 使用 1 个进程
                .warmupIterations(5) // 预热 5 次
                .measurementIterations(10) // 正式测试 10 次
                .build();
        new Runner(opt).run();
    }
}

JMH 要求基准测试类放在 src/main/java 下(而非 src/test/java),因为 JMH 会动态生成代码。

测试结果

Benchmark Mode Cnt Score Error Units
testReadWriteLockRead thrpt 10 40745.041 ± 5702.837 ops/ms
testReadWriteLockWrite thrpt 10 46714.948 ± 2200.964 ops/ms
testStampedLockRead thrpt 10 228822.159 ± 13864.537 ops/ms
testStampedLockWrite thrpt 10 53863.354 ± 3638.881 ops/ms
  • Benchmark:测试的方法名称(不同锁实现的读写性能对比)
  • Mode:测试模式(thrpt 表示吞吐量,单位是操作数/毫秒)
  • Cnt:迭代次数(每个测试运行了 10 次)
  • Score:平均吞吐量(值越大性能越好)
  • Error:误差范围(± 后的值,表示结果的波动性)
  • Units:单位(ops/ms 表示每毫秒完成的操作数)

结论

  • 读操作StampedLock 的吞吐量是 ReentrantReadWriteLock5 倍以上(因为乐观读无锁)。
  • 写操作:两者相差不大,StampedLock 稍快(因为无读锁竞争)。

YOLO