ReadWriteLock 和 StampedLock

62 阅读3分钟

ReentrantReadWriteLock

  现实中有这样一种场景:对共享资源有读和写的操作,且写操作没有读操作那么频繁。在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源;但是如果一个线程想去写这些共享资源,就不应该允许其他线程对该资源进行读和写的操作了。

  针对这种场景,JAVA 的并发包提供了读写锁 ReentrantReadWriteLock,它表示两个锁,一个是读操作相关的锁,称为共享锁;一个是写相关的锁,称为排他锁。

读读共享,其他都互斥,描述如下:

  • 线程进入读锁的前提条件:
    • 没有其他线程的写锁,
    • 没有写请求或者有写请求,但调用线程和持有锁的线程是同一个。
  • 线程进入写锁的前提条件:
    • 没有其他线程的读锁
    • 没有其他线程的写锁

使用

        ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
        // 获取写锁
        ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
        // 获取读锁
        ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
        // 写锁加锁和解锁
        writeLock.lock();
        writeLock.unlock();
        // 读锁加锁和解锁
        readLock.lock();
        readLock.unlock();

StampedLock

StampedLock 的主要特点概括一下,有以下几点:

  1. 所有获取锁的方法,都返回一个邮戳(Stamp),Stamp 为0表示获取失败,其余都表示成功;
  2. 所有释放锁的方法,都需要一个邮戳(Stamp),这个 Stamp 必须是和成功获取锁时得到的 Stamp 一致;
  3. StampedLock 是不可重入的;(如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁)
  4. StampedLock 有三种访问模式:
  • Reading(读模式):功能和ReentrantReadWriteLock的读锁类似
  • Writing(写模式):功能和ReentrantReadWriteLock的写锁类似
  • Optimistic reading(乐观读模式):这是一种优化的读模式。
  1. StampedLock 支持读锁和写锁的相互转换
    我们知道 RRW 中,当线程获取到写锁后,可以降级为读锁,但是读锁是不能直接升级为写锁的。
    StampedLock 提供了读锁和写锁相互转换的功能,使得该类支持更多的应用场景。
  2. 无论写锁还是读锁,都不支持 Conditon 等待

  我们知道,在 ReentrantReadWriteLock 中,当读锁被使用时,如果有线程尝试获取写锁,该写线程会阻塞。
  但是,在 Optimistic reading 中,即使读线程获取到了读锁,写线程尝试获取写锁也不会阻塞,这相当于对读模式的优化,但是可能会导致数据不一致的问题。所以,当使用 Optimistic reading 获取到读锁时,必须对获取结果进行校验。

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