前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >比读写锁更快的 StampedLock

比读写锁更快的 StampedLock

作者头像
码哥字节
发布2020-03-24 15:18:55
8930
发布2020-03-24 15:18:55
举报
文章被收录于专栏:Java 技术栈

预计阅读所需时间 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()。这个规则一定要记清楚。
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-10-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 码哥字节 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • StampedLock 支持的三种锁模式
  • StampedLock 代码示例
  • StampedLock 使用注意事项
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档