比读写锁更快的 StampedLock

2020-03-24 15:18:55 浏览数 (1)

预计阅读所需时间 7 分钟,建议收藏

我们先回顾上一篇 ReentrantReadWriteLock 读写锁,为什么有了 ReentrantReadWriteLock,还要引入 StampLock

ReentrantReadWriteLock 使得多个读线程同时持有读锁(只要写未被占用),而写锁是独占的。但是很容易造成 “饥饿问题”:

读线程非常多,写线程很少的情况下,很容易导致写线程 “饥饿”

StampedLock 支持的三种锁模式

我们先来看看在使用上StampedLock 和上一篇文章讲的 ReadWriteLock 有哪些区别。

  1. ReadWriteLock 支持两种模式:一种是读锁,一种是写锁。写锁独占,读读共享、读写互斥。
  2. StampedLock 支持三种模式,分别是:写锁悲观读锁乐观读。其中,写锁、悲观读锁的语义和 ReadWriteLock 的写锁、读锁的语义非常类似,允许多个线程同时获取悲观读锁,但是只允许一个线程获取写锁,写锁和悲观读锁是互斥的。不同的是:StampedLock 里的写锁和悲观读锁加锁成功之后,都会返回一个 stamp;然后解锁的时候,需要传入这个 stamp。

StampedLock 支持读锁和写锁的相互转换 我们知道 RRW 中,当线程获取到写锁后,可以降级为读锁,但是读锁是不能直接升级为写锁的。StampedLock 提供了读锁和写锁相互转换的功能,使得该类支持更多的应用场景。

之所以性能比 ReentrantReadWriteLock好,其关键就是支持乐观读。ReadWriteLock 支持多个线程同时读,但是当多个线程同时读的时候,所有的写操作会被阻塞;

**而 StampedLock 提供的乐观读,是允许一个线程获取写锁的,也就是说不是所有的写操作都被阻塞。**

注意这里是乐观读,并不是 “乐观读锁”,其实它是无锁的,其实它跟数据库的乐观锁有异曲同工之妙。

乐观锁的实现很简单,在生产订单的表 product_doc 里增加了一个数值型版本号字段 version,每次更新 product_doc 这个表的时候,都将 version 字段加 1。

代码语言:javascript复制
select id,... ,version
from product_doc
where id=777

而更新的时候匹配 version 才更新。

代码语言:javascript复制
update product_doc
set version=version 1,...
where id=777 and version=9

你会发现数据库里的乐观锁,查询的时候需要把 version 字段查出来,更新的时候要利用 version 字段做验证。这个 version 字段就类似于 StampedLock 里面的 stamp。这样对比着看,相信你会更容易理解 StampedLock 里乐观读的用法。

StampedLock 代码示例

代码语言:javascript复制
class Point {
    // 共享变量 x、y 坐标
    private double x, y;
    private final StampedLock sl = new StampedLock();

    /**
     * 移动坐标
     *
     * @param deltaX
     * @param deltaY
     */
    public void move(double deltaX, double deltaY) {
        long stamp = sl.writeLock(); //涉及到对共享资源的修改,使用写锁-独占
        try {
            x  = deltaX;
            y  = deltaY;
        } finally {
            sl.unlockWrite(stamp);
        }
    }

    /**
     * 使用乐观读访问共享资源:计算到原点的距离。
     *  注意:乐观读锁在保证数据一致性上需要拷贝一份要操作的变量到方法栈,并且在操作数据时候      * 可能其他写线程已经修改了数据,
     * 而我们操作的是方法栈里面的数据,也就是一个快照,所以最多返回的不是最新的数据,但是一致性还是得到保障的。
     *
     * @return
     */
    public 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);
    }

    /**
     * 读锁转换写锁:若当前坐标在原点则移动
     *
     * @param newX
     * @param newY
     */
    public void moveIfAtOrigin(double newX, double newY) {
        // 不能直接使用乐观读,不是只读的方法
        long stamp = sl.readLock();
        try {
            while (x == 0.0 && y == 0.0) {
                //转换为写锁,若返回值不等于 0 则获取写锁成功
                long ws = sl.tryConvertToWriteLock(stamp);
                if (ws != 0L) {
                    // 转换写锁后,操作共享变量
                    stamp = ws;
                    x = newX;
                    y = newY;
                    break;
                } else {
                    // 转换写锁失败则先释放读锁,再尝试获取写锁
                    sl.unlockRead(stamp);
                    stamp = sl.writeLock();
                }
            }
        } finally {
            sl.unlock(stamp);
        }
    }
}

上述例子中,特殊的就是 distanceFromOrigin()moveIfAtOrigin() 方法,第一个方法使用了 乐观读,让读写可以并发执行,通过上面例子我们也总结出 乐观读的使用模板。第二个则是使用了读锁转换成写锁的方式。

代码语言:javascript复制
long stamp = lock.tryOptimisticRead();  // 乐观读
copyVaraibale2ThreadMemory();           // 拷贝变量到线程本地堆栈
if(!lock.validate(stamp)){              // 校验是否被修改
    long stamp = lock.readLock();       // 获取悲观读锁
    try {
        copyVaraibale2ThreadMemory();   // 拷贝变量到线程本地堆栈
     } finally {
       lock.unlock(stamp);              // 释放悲观锁
    }

}
useThreadMemoryVarables();             // 使用局部变量进行数据操作

StampedLock 使用注意事项

对于读多写少的场景 StampedLock 性能很好,简单的应用场景基本上可以替代 ReadWriteLock,但是StampedLock 的功能仅仅是 ReadWriteLock 的子集,在使用的时候,还是有几个地方需要注意一下。

  1. StampedLock 在命名上并没有增加 Reentrant,想必你已经猜测到 StampedLock 应该是不可重入的。事实上,的确是这样的,StampedLock 不支持重入。这个是在使用中必须要特别注意的。
  2. 另外,StampedLock 的悲观读锁、写锁都不支持条件变量,这个也需要注意 。
  3. 如果线程阻塞在 StampedLock 的 readLock() 或者 writeLock() 上时,此时调用该阻塞线程的 interrupt() 方法,会导致 CPU 飙升。所以,使用 StampedLock 一定不要调用中断操作,如果需要支持中断功能,一定使用可中断的悲观读锁 readLockInterruptibly() 和写锁 writeLockInterruptibly()。这个规则一定要记清楚。

0 人点赞