深入理解Java8并发工具类StampedLock

StampedLock类是JDK8里面新增的一个并发工具类,这个类比较特殊,在此之前我们先简单的了解一下关于数据库或者存储系统的锁策略和机制。

总体上来说锁有两大类:

悲观锁:总是认为会有冲突发生,所以每次操作临界区资源时都会加锁。

乐观锁:顾名思义,认为每次操作临界区资源时不会发生冲突,但会先记录一个版本号,在提交事务时,会检查版本号是否变更,从而作出判断放弃或者重试。

对于一个高并发的应用程序来说,数据库常常会成为一个访问的瓶颈,这里面主要存在以下的几种访问情况:

(1)读读并发

(2)读写并发

(3)写写并发

一般情况下,数据库都会有读共享写独占的锁并发的方案,也就是说读读并发是没问题的,但在读写并发时,则有可能出现读取不一致情况,也就是常说的脏读,所以在悲观锁的模式下,在有写线程的时候,是不允许有任何其他的读和写线程的,也就是说写是独占的,这样会导致系统的吞吐明显下降,如何避免这一情况,于是就出现了基于MVCC多版本控制并发的策略,在这种策略下读写并发是可以同时进行的,底层的原理是当前有并发的写线程在独占,那么读线程就直接读取事务log里面的历史最新版本的数据,这样以来就大大提高了并发吞吐能力,虽然读取的数据并不是最新的数据,但是历史上最新的,同时也保持了一致性,目前主流的数据库都支持这种模式。最后一种是写写并发场景,这种场景通常基于乐观锁的并发写方案也称OCC,多个并发的写线程,每个线程都不会修改原始数据,而是从原始数据上拷贝上一份数据,同时记录版本号,不同的线程更新自己的数据,在最终写会时会判断版本号是否变更,如果变更则意味有人已经更改过了,那么当前线程需要做的就是自旋重试,如果重试指定的次数依然失败,那么就应该放弃更新,这种策略仅仅适合写并发并不强烈的场景,如果写竞争严重,那么多次自旋重试的开销也是非常耗性能的,如果竞争激烈,那么写锁独占的方式则更加适合。

基于上面谈到的这些内容,我们再来分析StampedLock类,就会非常比较容易理解,它实际主要解决的是读写并发场景更加类似于上面我们谈到的MVCC的模式。

StampedLock类有三种模式:

(一)写锁,这里的写锁是独占和排它的,这里对于申请写锁成功的线程会得到一个stamp,在释放锁unlockWrite(long)的时候会传入这个票据,申请写锁还支持非阻塞模式的调用通过tryWriteLock方法或者可超时的申请,处于写锁状态下,任何其他的写锁,读锁,乐观读锁都会失败。

(二)读锁,申请成功会返回一个票据,同理在释放的时候unlockRead(long)也需要传回票据。读锁是共享的,前提是没有任何写锁占用。

(三)乐观读锁,是新的特性,这种策略非常轻量级,在操作数据时候并没有使用CAS来设置锁的状态,如果当前没有线程持有写锁,那么乐观读锁就会立即返回一个非0的票据,在获得之后,为了保持一致性,要拷贝需要使用的相关数据到线程的的栈里面,然后再次判断票据是否有效,如果无效,则意味着这期间有线程修改了数据状态,所以这时候要么放弃操作,要么直接申请读锁,如果票据有效则意味着,当前的数据没有被更改过,可能不是最新的,但是一致的,在读写并发时候,用来读取是没有问题的,所以效率会高很多,因为没有使用任何的加锁操作,我们可以理解读取的数据类似MVCC的快照,最多不是最新的,但一定是一致的。

StampedLock类的主要特点,我认为有两个:

(1)通过乐观读锁支持读写并发,这里使用的是票据对比。

(2)支持读锁升级成写锁

下面我们看一下官网给出的例子,分别展示了写锁,乐观读锁和读锁升级成写锁的案例:

class Point {

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

    // 乐观读锁(tryOptimisticRead)
    double distanceFromOrigin() {

        // 尝试获取乐观读锁(1)
        long stamp = sl.tryOptimisticRead();
        // 将全部变量拷贝到方法体栈内(2)
        double currentX = x, currentY = y;
        // 检查在(1)获取到读锁票据后,锁有没被其他写线程排它性抢占(3)
        if (!sl.validate(stamp)) {
            // 如果被抢占则获取一个共享读锁(悲观获取)(4)
            stamp = sl.readLock();
            try {
                // 将全部变量拷贝到方法体栈内(5)
                currentX = x;
                currentY = y;
            } finally {
                // 释放共享读锁(6)
                sl.unlockRead(stamp);
            }
        }
        // 返回计算结果(7)
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }

    // 读锁升级成写锁
    void moveIfAtOrigin(double newX, double newY) {
        // 这里可以使用乐观读锁替换(1)
        long stamp = sl.readLock();
        try {
            // 如果当前点在原点则移动(2)
            while (x == 0.0 && y == 0.0) {
                // 尝试将获取的读锁升级为写锁(3)
                long ws = sl.tryConvertToWriteLock(stamp);
                // 升级成功,则更新票据,并设置坐标值,然后退出循环(4)
                if (ws != 0L) {
                    stamp = ws;
                    x = newX;
                    y = newY;
                    break;
                } else {
                    // 读锁升级写锁失败则释放读锁,显示获取独占写锁,然后循环重试(5)
                    sl.unlockRead(stamp);
                    stamp = sl.writeLock();
                }
            }
        } finally {
            // 释放锁(6)
            sl.unlock(stamp);
        }
    }
}

总结:

本文主要介绍了JDK8里面新增的并发工具类StampedLock,相比ReentrantLock重入锁提供了更好的性能,并且支持读写并发的场景和读锁升级成写锁的功能,在使用时候一定注意乐观读锁需要先获取票据,然后在拷贝实例数据到线程栈,然后接着判断票据是否有效,如果位置搞反,那么则有可能使用出错,这一点需要注意。最后我们还要记住StampedLock是不支持重入的,尽管你可以通过锁转换来变相实现,还有对于StampedLock锁这里并没有明确强调公平和非公平的概念,这里StampedLock会尽量保证最好的性能。

https://docs.oracle.com/javase/8/docs/api/

https://www.jianshu.com/p/481071ddafd3

https://netjs.blogspot.com/2016/08/stampedlock-in-java.html

原文发布于微信公众号 - 我是攻城师(woshigcs)

原文发表时间:2018-08-17

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏农夫安全

sqlmap被ban了ip怎么办

sqlmap被ban了ip怎么办 第一种办法 好不容易挖到的注入点,结果总是因为请求速度过快被ban掉ip,我觉得可以给sqlmap加个代理池!暑假前的想法,...

5696
来自专栏Java帮帮-微信公众号-技术文章全总结

Web-第十天 Cookie&Session学习

当用户访问某些Web应用时,经常会显示出该用户上一次的访问时间。例如,QQ登录成功后,会显示用户上次的登录时间。通过本任务,读者将学会如何使用Cookie技术实...

1493
来自专栏企鹅号快讯

Python接口自动化-4-HTTPS请求

Requests 可以为 HTTPS 请求验证 SSL 证书,就像 web 浏览器一样。SSL 验证默认是开启的。 verify参数: 默认verify=Tru...

2899
来自专栏小勇DW3

网页爬虫小记:两种方式的爬取网站内容

此处进行简单的分类,对于普通的网页爬取内容,如果没有登录界面可以直接使用Jsoup的API进行爬取;

1782
来自专栏*坤的Blog

公司web安全等级提升

公司的一个web数据展示系统,本来是内网的,而且是一个单独的主机,不存在远程控制的问题,所以之前并没有考虑一些安全相关的测试.但是国调安全检查的需要添加这样子的...

1654
来自专栏Zachary46

Python爬取qq空间说说

我的博客即将搬运同步至腾讯云+社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invi...

1403
来自专栏macOS 开发学习

macOS 应用开发小集锦

输出结果与当前app的语言环境有关(默认为English),如果需要修改工程的语言环境,需要设置Edit Scheme...

942
来自专栏NetCore

Struts原理与实践

一、JDBC的工作原理 Struts在本质上是java程序,要在Struts应用程序中访问数据库,首先,必须搞清楚Java Database Connect...

2148
来自专栏落花落雨不落叶

vue+sass 下sass不能运行问题

3698
来自专栏Linux驱动

第1阶段——uboot分析之硬件初始化start.S(4)

分析uboot第一个执行函数_start(cpu/arm920t/start.S)  打开cpu/arm920t/start.S 1 .globl _start...

2308

扫码关注云+社区