Java 入门指南:Java 并发编程 —— StampedLock 读写锁

StampedLock

StampedLock 是 Java 8 引入的一种新型锁机制,它综合了读写锁、乐观读锁和悲观写锁的特点,旨在提供更高效的并发访问控制。有人称它为锁的性能之王。

StampedLock 没有实现 Lock 接口和 ReadWriteLock 接口,但它实现了 读写锁 的功能,并且性能比 ReentrantReadWriteLock 更高。StampedLock 还把读锁分为了 乐观读锁悲观读锁 两种。

它的核心思想在于,在读的时候如果发生了写,应该通过重试的方式来获取新的值,而不应该阻塞写操作。

这种模式也就是典型的无锁编程思想,和 CAS 自旋的思想一样。这种操作方式决定了 StampedLock 十分适用于读线程非常多而写线程非常少的场景,同时还避免了写饥饿情况的发生。

特点

StampedLock 的主要特点有:

  1. 无锁方式的乐观读锁机制,允许一个线程在没有任何写入操作发生的情况下读取数据,从而提高了性能。,避免了部分读操作的互斥开销。

  2. 可重入性:StampedLock 的可重入性发生在独占写锁的情况下。对于悲观读取锁和乐观读取锁,StampedLock不支持可重入性。

  3. 写锁的获得和释放是互斥的,写锁被持有时,其他的写锁和读锁都无法获取。

  4. 支持读写锁降级,可以将独占写锁转换为读锁,但不能将读锁转换为写锁。

  5. 不支持锁的升级,即不能将读锁升级为写锁。

  6. 没有监视器锁的内置锁饥饿,不会出现长时间阻塞的情况。

  7. API 复杂性:由于提供了乐观读锁和锁降级功能,StampedLock 的 API 相对复杂一些,需要更小心地使用以避免死锁和其他问题。ReentrantReadWriteLock 的 API 相对更直观和容易使用。

常用方法

  • tryOptimisticRead():返回一个乐观读取的戳记(stamp)。这是进行乐观读取的第一步。

  • validate(long stamp):检查给定的戳记是否仍然有效。如果自上次获取戳记以来没有写入操作发生,则返回 true;否则返回 false

  • readLock():获取一个读锁,阻止其他写入操作,但允许其他读取操作。

  • unlockRead(long stamp):释放之前获取的读锁。

  • writeLock():获取一个写锁,阻止其他所有读取和写入操作。

  • unlockWrite(long stamp):释放之前获取的写锁。

  • tryRead():尝试获取一个读锁。如果成功,返回一个有效的戳记;否则返回一个无效的戳记。

  • tryWrite():尝试获取一个写锁。如果成功,返回一个有效的戳记;否则返回一个无效的戳记。

StampedLock 模式

StampedLock 支持三种模式的访问控制:读锁悲观读锁乐观读锁

StampedLock 获取锁会返回一个 long 类型的变量(即 StampedLock 的 Stamp),释放锁的时候再把这个变量传进去

内部 long 变量

![[StampedLock state.png]]

StampedLock 用这个 long 类型的变量的前 7 位(LG_READERS)来表示读锁,每获取一个悲观读锁,就加 1(RUNIT),每释放一个悲观读锁,就减 1。

悲观读锁最多只能装 128 个(7 位限制),很容易溢出,所以用一个 int 类型的变量来存储溢出的悲观读锁

写锁用 state 变量剩下的位来表示,每次获取一个写锁,就加 0000 1000 0000(WBIT)。需要注意的是,写锁在释放的时候,并不是减 WBIT,而是再加 WBIT。这是为了让每次写锁都留下痕迹,解决 CAS 中的 ABA 问题,也为乐观锁检查变化 validate 方法提供基础。

对于乐观读锁,并没有真正改变 state 的值,而是在获取锁的时候记录 state 的写状态,在操作完成后去检查 state 的写状态部分是否发生变化。每次写锁都会留下痕迹,也是为了乐观读锁检查变化提供方便。

写模式

写模式(Exclusive mode):与传统的独占写锁类似,写模式下只允许一个线程独占访问临界区,其他线程无法获取读锁或乐观读锁。

StampedLock stampedLock = new StampedLock();
long stamp = stampedLock.writeLock();  // 获取写锁
stampedLock.unlockWrite(stamp);        // 释放写锁
悲观读模式

悲观读模式(Shared mode):与传统的共享读锁类似,读模式下允许多个线程同时读取共享资源,但不允许其他线程获取写锁和乐观读锁。

StampedLock stampedLock = new StampedLock();
long stamp = stampedLock.readLock();    // 获取悲观读锁
stampedLock.unlockRead(stamp);          // 释放悲观读锁
乐观读模式

乐观读模式(Optimistic mode):乐观读模式是一种特殊的读模式,假定在这个锁获取期间,共享变量不会被改变,不会阻塞写操作。

StampedLock stampedLock = new StampedLock();
long stamp = stampedLock.tryOptimisticRead(); // 获取乐观读锁

// 检查乐观读锁后是否有其他写锁,有则返回 false
stampedLock.validate(stamp); 

stampedLock.unlock(stamp); // 释放所有锁

在乐观读模式下,线程会尝试读取共享资源,并返回一个表示戳记(stamp)的值,的 戳记(stamp)可以是任意的整数值,用来标识特定的读操作

戳记(stamp)不是唯一的标识,不能用于表示并发版本控制或实现严格的数据一致性,它只是用于在乐观读模式下进行基本的验证。

当一个线程进入乐观读模式时,会获取当前的戳记,并将其保存下来。在读取共享资源后,线程会使用获取时的戳记再次校验共享资源是否发生了写入操作。

如果验证通过,即没有其他线程进行了写入,那么乐观读操作是有效的;如果验证失败,意味着共享资源可能被其他线程修改过,这时候可以选择重试读操作或者进一步获取悲观读锁。

每次共享资源发生写入操作时,戳记的值都会发生变化。因此,在乐观读模式下,通过比较读取时的戳记和当前的戳记,可以判断共享资源是否被修改。

如果在读取期间没有写操作发生,那么读操作是安全的,否则会导致数据不一致。乐观读模式可以用于读操作频繁,写操作较少和不严格一致性要求的场景

使用示例

下面是一个详细的 StampedLock 使用示例,包括如何进行乐观读取、写入操作以及如何处理读写冲突。我们将创建一个简单的示例,其中包含一个共享资源 x,多个线程将尝试读取和修改这个资源。

import java.util.concurrent.locks.StampedLock;
import java.util.concurrent.TimeUnit;

public class StampedLockExample {

    private double x;
    private final StampedLock sl = new StampedLock();

    public static void main(String[] args) {
        StampedLockExample example = new StampedLockExample();

        // 启动一个写入线程
        Thread writerThread = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                example.writeOperation(i);
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        });
        writerThread.start();

        // 启动多个读取线程
        for (int i = 0; i < 5; i++) {
            Thread readerThread = new Thread(() -> {
                while (true) {
                    double readValue = example.optimisticRead();
                    System.out.println("Read value: " + readValue);
                    try {
                        TimeUnit.MILLISECONDS.sleep(200);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                }
            });
            readerThread.start();
        }

        try {
            writerThread.join();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    public void writeOperation(int newValue) {
        long stamp = sl.writeLock(); // 获取写锁
        try {
            x = calculateNewValue(newValue);
            System.out.println("Updated value: " + x);
        } finally {
            sl.unlockWrite(stamp); // 释放写锁
        }
    }

    public double optimisticRead() {
        long stamp = sl.tryOptimisticRead(); // 尝试乐观读
        double current = x;
        if (!sl.validate(stamp)) { // 检查数据是否被修改
            stamp = sl.readLock(); // 获取读锁
            try {
                current = x;
            } finally {
                sl.unlockRead(stamp); // 释放读锁
            }
        }
        return current;
    }

    private double calculateNewValue(int newValue) {
        // 这里执行计算新的值
        return newValue * 10.0; // 示例代码
    }
}

示例说明:

  1. 初始化共享资源:我们定义了一个共享资源 x,它将在写入操作中被更新。

  2. 创建 StampedLock 实例sl 是一个 StampedLock 实例,用于管理读写操作。

  3. 写入操作writeOperation 方法获取一个写锁,更新 x 的值,并释放写锁。在本例中,写入操作会将 newValue 乘以 10 作为新的 x 值。

  4. 乐观读取optimisticRead 方法首先尝试进行乐观读取。如果数据在读取过程中被修改,则重新获取读锁,读取最新的数据值。读取完成后释放读锁。

  5. 主函数

    • 启动一个写入线程,每隔一秒更新一次 x 的值。
    • 启动多个读取线程,每隔 200 毫秒读取一次 x 的值。
    • 读取线程将输出读取到的值。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值