JDK8并发包新增StampedLock锁
4.8 JDK8新增的StampedLock锁探究
StampedLock是并发包里面jdk8版本新增的一个锁,该锁提供了三种模式的读写控制,三种模式分别如下:
- 写锁writeLock,是个排它锁或者叫独占锁,同时只有一个线程可以获取该锁,当一个线程获取该锁后,其它请求的线程必须等待,当目前没有线程持有读锁或者写锁的时候才可以获取到该锁,请求该锁成功后会返回一个stamp票据变量用来表示该锁的版本,当释放该锁时候需要unlockWrite并传递参数stamp。
- 悲观读锁readLock,是个共享锁,在没有线程获取独占写锁的情况下,同时多个线程可以获取该锁,如果已经有线程持有写锁,其他线程请求获取该读锁会被阻塞。这里讲的悲观其实是参考数据库中的乐观悲观锁的,这里说的悲观是说在具体操作数据前悲观的认为其他线程可能要对自己操作的数据进行修改,所以需要先对数据加锁,这是在读少写多的情况下的一种考虑,请求该锁成功后会返回一个stamp票据变量用来表示该锁的版本,当释放该锁时候需要unlockRead并传递参数stamp。
- 乐观读锁tryOptimisticRead,是相对于悲观锁来说的,在操作数据前并没有通过CAS设置锁的状态,如果当前没有线程持有写锁,则简单的返回一个非0的stamp版本信息,获取该stamp后在具体操作数据前还需要调用validate验证下该stamp是否已经不可用,也就是看当调用tryOptimisticRead返回stamp后到到当前时间间是否有其他线程持有了写锁,如果是那么validate会返回0,否者就可以使用该stamp版本的锁对数据进行操作。由于tryOptimisticRead并没有使用CAS设置锁状态所以不需要显示的释放该锁。该锁的一个特点是适用于读多写少的场景,因为获取读锁只是使用与或操作进行检验,不涉及CAS操作,所以效率会高很多,但是同时由于没有使用真正的锁,在保证数据一致性上需要拷贝一份要操作的变量到方法栈,并且在操作数据时候可能其他写线程已经修改了数据,而我们操作的是方法栈里面的数据,也就是一个快照,所以最多返回的不是最新的数据,但是一致性还是得到保障的。
下面通过JDK8注释里面的一个例子讲解来加深对上面讲解的理解。
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); } } }
如上代码Point类里面有两个成员变量,和三个操作成员变量的方法,另外实例化了一个StampedLock对象用来保证操作的原子性。
首先分析下move方法,该函数作用是在添加增量,改变当前point坐标的位置,代码先获取到了写锁,然后对point坐标进行修改,然后释放锁。该锁是排它锁,这保证了其他线程调用move函数时候会被阻塞,直到当前线程显示释放了该锁,也就是保证了对变量x,y操作的原子性。
然后看下distanceFromOrigin方法,该方法作用是计算当前位置到原点的距离,代码(1)首先尝试获取乐观读锁,如果当前没有其它线程获取到了写锁,那么(1)会返回一个非0的stamp用来表示版本信息,代码(2)拷贝变量到本地方法栈里面,代码(3)检查在(1)获取到的票据是否还有效,之所以还要在此校验是因为代码(1)获取读锁时候并没有通过CAS操作修改锁的状态而是简单的通过与或操作返回了一个版本信息,这里校验是看在在获取版本信息到现在的时间段里面是否有其他线程持有了写锁,如果有则之前获取的版本信息就无效了。这里如果校验成功则执行(7)使用本地方法栈里面的值进行计算然后返回。需要注意的是在代码(3)校验成功后,代码(7)计算中其他线程可能获取到了写锁并且修改了x,y的值,而当前线程执行代码(7)进行计算时候采用的才是对修改前值的拷贝,也就是操作的值是对之前值的一个拷贝,并不是新的值。另外还有个问题,代码(2)和(3)能否互换,答案是不能,假设位置换了,那么首先执行validate,假如验证通过了,要拷贝x,y值到本地方法栈,而在拷贝的过程中很有可能其他线程已经修改了x,y中的一个,这就造成了数据的不一致性了。那么你可能会问,那不交换(2)和(3)时候在拷贝x,y值到本地方法栈里面时候也会存在其他线程修改了x,y中的一个值那,这个确实会存在,但是,别忘了拷贝后还有一道validate,如果这时候有线程修改了x,y中的值,那么肯定是有线程在调用validate前sl.tryOptimisticRead后获取了写锁,那么validate时候就会失败。现在应该明白了吧,这也是乐观读设计的精妙之处也是使用时候容易出问题的地方。下面继续分析validate失败后会执行代码(4)获取悲观读锁,如果这时候骑行线程持有写锁则代码(4)会导致的当前线程阻塞直到其它线程释放了写锁。获取到读锁后,代码(5)拷贝变量到本地方法栈,然后就是代码(6)释放了锁,拷贝的时候由于加了读锁在拷贝期间其它线程获取写锁时候会被阻塞,这保证了数据的一致性。最后代码(7)使用方法栈里面数据计算返回,同理这里在计算时候使用的数据也可能不是最新的,其它写线程可能已经修改过原来的x,y值了。
最后一个方法moveIfAtOrigin方法作用是如果当前坐标为原点则移动到指定的位置。代码(1)获取悲观读锁,保证其它线程不能获取写锁修改x,y值,然后代码(2)判断如果当前点在原点则更新坐标,代码(3)尝试升级读锁为写锁,这里升级不一定成功,因为多个线程都可以同时获取悲观读锁,当多个线程都执行到(3)时候只有一个可以升级成功,升级成功则返回非0的stamp,否非返回0,这里假设当前线程升级成功,然后执行步骤(4)更新stamp值和坐标值然后退出循环,如果升级失败则执行步骤(5)首先释放读锁然后申请写锁,获取到写锁后在循环重新设置坐标值。最后步骤(6)释放锁。
使用乐观读锁还是很容易犯错误的,必须要小心,必须要保证如下的使用顺序:
long stamp = lock.tryOptimisticRead(); //非阻塞获取版本信息 copyVaraibale2ThreadMemory();//拷贝变量到线程本地堆栈 if(!lock.validate(stamp)){ // 校验 long stamp = lock.readLock();//获取读锁 try { copyVaraibale2ThreadMemory();//拷贝变量到线程本地堆栈 } finally { lock.unlock(stamp);//释放悲观锁 } } useThreadMemoryVarables();//使用线程本地堆栈里面的数据进行操作
总结: 相比ReentrantLock读写锁,StampedLock通过提供乐观读锁在多线程多写线程少的情况下提供更好的性能,因为乐观读锁不需要进行CAS设置锁的状态而只是简单的测试状态。更具体测试数据期待Java并发编程基础之并发包源码剖析一书的出版。
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
前端性能优化之重排和重绘
前言,最近利用碎片时间拜读了一下尼古拉斯的另一巨作《高性能JavaScript》,今天写的文章从“老生常谈”的页面重绘和重排入手,去探究这两个概念在页面性能提升上的作用。 一.重排 & 重绘 有经验的大佬对这个概念一定不会陌生,“浏览器输入URL发生了什么”。估计大家已经烂熟于心了,从计算机网络到JS引擎,一路飞奔到浏览器渲染引擎。 经验越多就能理解的越深。感兴趣的同学可以看一下这篇文章,深度和广度俱佳从输入 URL 到页面加载的过程?如何由一道题完善自己的前端知识体系! 切回正题,我们继续探讨何为重排。浏览器下载完页面所有的资源后,就要开始构建DOM树,于此同时还会构建渲染树(Render Tree)。(其实在构建渲染树之前,和DOM树同期会构建Style Tree。DOM树与Style Tree合并为渲染树) DOM树 表示页面的结构 渲染树 表示页面的节点如何显示 一旦渲染树构建完成,就要开始绘制(paint)页面元素了。当DOM的变化引发了元素几何属性的变化,比如改变元素的宽高,元素的位置,导致浏览器不得不重新计算元素的几何属性,并重新构建渲染树,这个过程称为“重排”。...
- 下一篇
TiDB - 如何在国内编译
0: 由于天朝的特殊,在国内很不好编译(主要是依赖库下载不了)。 所以记录下编译过程 需要的工具: debian 或者其他linux其他版本。 make,git,golang(最好1.11版本以上) 编译过程 export GOPATH=/data/tidb mkdir -p /data/tidb/src/github.com/pingcap cd $GOPATH/src/github.com/pingcap git clone https://github.com/pingcap/tidb go.mod 在后面添加 replace ( cloud.google.com/go => github.com/google/go-cloud v0.4.1-0.20181112030950-0b43d4400e53 go.uber.org/atomic => github.com/uber-go/atomic v1.3.3-0.20181018215023-8dc6146f7569 go.uber.org/multierr => github.com/uber-go/m...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- SpringBoot2初体验,简单认识spring boot2并且搭建基础工程
- CentOS7,8上快速安装Gitea,搭建Git服务器
- Linux系统CentOS6、CentOS7手动修改IP地址
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- MySQL8.0.19开启GTID主从同步CentOS8
- Red5直播服务器,属于Java语言的直播服务器
- SpringBoot2更换Tomcat为Jetty,小型站点的福音
- SpringBoot2整合Redis,开启缓存,提高访问速度
- SpringBoot2整合Thymeleaf,官方推荐html解决方案
- CentOS7,CentOS8安装Elasticsearch6.8.6