多线程系列(十一) -浅析并发读写锁StampedLock
一、摘要
在上一篇文章中,我们讲到了使用ReadWriteLock可以解决多线程同时读,但只有一个线程能写的问题。
如果继续深入的分析ReadWriteLock,从锁的角度分析,会发现它有一个潜在的问题:如果有线程正在读数据,写线程准备修改数据的时候,需要等待读线程释放锁后才能获取写锁,简单的说就是,读的过程中不允许写,这其实是一种悲观的读锁。
为了进一步的提升程序并发执行效率,Java 8 引入了一个新的读写锁:StampedLock。
与ReadWriteLock相比,StampedLock最大的改进点在于:在原先读写锁的基础上,新增了一种叫乐观读的模式。该模式并不会加锁,因此不会阻塞线程,程序会有更高的执行效率。
什么是乐观锁和悲观锁呢?
- 乐观锁:就是乐观的估计读的过程中大概率不会有写入,因此被称为乐观锁
- 悲观锁:指的是读的过程中拒绝有写入,也就是写入必须等待
显然乐观锁的并发执行效率会更高,但一旦有数据的写入导致读取的数据不一致,需要能检测出来,再读一遍就行。
下面我们一起来了解一下StampedLock的用法!
二、StampedLock
StampedLock的使用方式比较简单,只需要实例化一个StampedLock对象,然后调用对应的读写方法即可,它有三个核心方法如下!
readLock():表示读锁,多个线程读不会阻塞,效果与ReadWriteLock的读锁模式类似writeLock():表示写锁,同一时刻有且只有一个写线程能获取锁资源,效果与ReadWriteLock的写锁模式类似tryOptimisticRead():表示乐观读,并没有加锁,它用于非常短的读操作,允许多个线程同时读
其中readLock()和writeLock()方法,与ReadWriteLock的效果完全一致,在此就不重复演示了。
下面我们来看一个tryOptimisticRead()方法的简单使用示例。
2.1、tryOptimisticRead 方法
public class CounterDemo {
private final StampedLock lock = new StampedLock();
private int count;
public void write() {
// 1.获取写锁
long stamp = lock.writeLock();
try {
count++;
// 方便演示,休眠一下
sleep(200);
println("获得了写锁,count:" + count);
} finally {
// 2.释放写锁
lock.unlockWrite(stamp);
}
}
public int read() {
// 1.尝试通过乐观读模式读取数据,非阻塞
long stamp = lock.tryOptimisticRead();
// 2.假设x = 0,但是x可能被写线程修改为1
int x = count;
// 方便演示,休眠一下
int millis = new Random().nextInt(500);
sleep(millis);
println("通过乐观读模式读取数据,value:" + x + ", 耗时:" + millis);
// 3.检查乐观读后是否有其他写锁发生
if(!lock.validate(stamp)){
// 4.如果有,采用悲观读锁,并重新读取数据到当前线程局部变量
stamp = lock.readLock();
try {
x = count;
println("乐观读后检查到数据发生变化,获得了读锁,value:" + x);
} finally{
// 5.释放悲观读锁
lock.unlockRead(stamp);
}
}
// 6.返回读取的数据
return x;
}
private void sleep(long millis){
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void println(String message){
String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS").format(new Date());
System.out.println(time + " 线程:" + Thread.currentThread().getName() + " " + message);
}
}
public class MyThreadTest {
public static void main(String[] args) throws InterruptedException {
CounterDemo counter = new CounterDemo();
Runnable readRunnable = new Runnable() {
@Override
public void run() {
counter.read();
}
};
Runnable writeRunnable = new Runnable() {
@Override
public void run() {
counter.write();
}
};
// 启动3个读线程
for (int i = 0; i < 3; i++) {
new Thread(readRunnable).start();
}
// 停顿一下
Thread.sleep(300);
// 启动3个写线程
for (int i = 0; i < 3; i++) {
new Thread(writeRunnable).start();
}
}
}
看一下运行结果:
2023-10-25 13:47:16:952 线程:Thread-0 通过乐观读模式读取数据,value:0, 耗时:19
2023-10-25 13:47:17:050 线程:Thread-2 通过乐观读模式读取数据,value:0, 耗时:172
2023-10-25 13:47:17:247 线程:Thread-1 通过乐观读模式读取数据,value:0, 耗时:369
2023-10-25 13:47:17:382 线程:Thread-3 获得了写锁,count:1
2023-10-25 13:47:17:586 线程:Thread-4 获得了写锁,count:2
2023-10-25 13:47:17:788 线程:Thread-5 获得了写锁,count:3
2023-10-25 13:47:17:788 线程:Thread-1 乐观读后检查到数据发生变化,获得了读锁,value:3
从日志上可以分析得出,读线程Thread-0和Thread-2在启动写线程之前就已经执行完,因此没有进入竞争读锁阶段;而读线程Thread-1因为在启动写线程之后才执行完,这个时候检查到数据发生变化,因此进入读锁阶段,保证读取的数据是最新的。
和ReadWriteLock相比,StampedLock写入数据的加锁过程基本类似,不同的是读取数据。
读取数据大致的过程如下:
- 1.尝试通过
tryOptimisticRead()方法乐观读模式读取数据,并返回版本号 - 2.数据读取完成后,再通过
lock.validate()去验证版本号,如果在读取过程中没有写入,版本号不会变,验证成功,直接返回结果 - 3.如果在读取过程中有写入,版本号会发生变化,验证将失败。在失败的时候,再通过悲观读锁再次读取数据,把读取的最新结果返回
对于读多写少的场景,由于写入的概率不高,程序在绝大部分情况下可以通过乐观读获取数据,极少数情况下使用悲观读锁获取数据,并发执行效率得到了大大的提升。
乐观锁实际用途也非常广泛,比如数据库的字段值修改,我们举个简单的例子。
在订单库存表上order_store,我们通常会增加了一个数值型版本号字段version,每次更新order_store这个表库存数据的时候,都将version字段加1,同时检查version的值是否满足条件。
select id,... ,version
from order_store
where id = 1000
update order_store
set version = version + 1,...
where id = 1000 and version = 1
数据库的乐观锁,就是查询的时候将version查出来,更新的时候利用version字段验证是否一致,如果相等,说明数据没有被修改,读取的数据安全;如果不相等,说明数据已经被修改过,读取的数据不安全,需要重新读取。
这里的version就类似于StampedLock的stamp值。
2.2、tryConvertToWriteLock 方法
其次,StampedLock还提供了将悲观读锁升级为写锁的功能,对应的核心方法是tryConvertToWriteLock()。
它主要使用在if-then-update的场景,即:程序先采用读模式,如果读的数据满足条件,就返回;如果读的数据不满足条件,再尝试写。
简单示例如下:
public int readAndWrite(Integer newCount) {
// 1.获取读锁,也可以使用乐观读
long stamp = lock.readLock();
int currentValue = count;
try {
// 2.检查是否读取数据
while (Objects.isNull(currentValue)) {
// 3.如果没有,尝试升级写锁
long wl = lock.tryConvertToWriteLock(stamp);
// 4.不为 0 升级写锁成功
if (wl != 0L) {
// 重新赋值
stamp = wl;
count = newCount;
currentValue = count;
break;
} else {
// 5.升级失败,释放之前加的读锁并上写锁,通过循环再试
lock.unlockRead(stamp);
stamp = lock.writeLock();
}
}
} finally {
// 6.释放最后加的锁
lock.unlock(stamp);
}
// 7.返回读取的数据
return currentValue;
}
三、小结
总结下来,与ReadWriteLock相比,StampedLock进一步把读锁细分为乐观读和悲观读,能进一步提升了并发执行效率。
好处是非常明显的,系统性能得到提升,但是代价也不小,主要有以下几点:
- 1.代码逻辑更加复杂,如果编程不当很容易出 bug
- 2.
StampedLock是不可重入锁,不能在一个线程中反复获取同一个锁,如果编程不当,很容易出现死锁 - 3.如果线程阻塞在
StampedLock的readLock()或者writeLock()方法上时,此时试图通过interrupt()方法中断线程,会导致 CPU 飙升。因此,使用StampedLock一定不要调用中断操作,如果需要支持中断功能,推荐使用可中断的读锁readLockInterruptibly()或者写锁writeLockInterruptibly()方法。
最后,在实际的使用过程中,乐观读编程模型,推荐可以按照以下固定模板编写。
public int read() {
// 1.尝试通过乐观读模式读取数据,非阻塞
long stamp = lock.tryOptimisticRead();
// 2.假设x = 0,但是x可能被写线程修改为1
int x = count;
// 3.检查乐观读后是否有其他写锁发生
if(!lock.validate(stamp)){
// 4.如果有,采用悲观读锁,并重新读取数据到当前线程局部变量
stamp = lock.readLock();
try {
x = count;
} finally{
// 5.释放悲观读锁
lock.unlockRead(stamp);
}
}
// 6.返回读取的数据
return x;
}
四、参考
1、https://www.liaoxuefeng.com/wiki/1252599548343744/1309138673991714
2、https://zhuanlan.zhihu.com/p/257868603
五、写到最后
最近无意间获得一份阿里大佬写的技术笔记,内容涵盖 Spring、Spring Boot/Cloud、Dubbo、JVM、集合、多线程、JPA、MyBatis、MySQL 等技术知识。需要的小伙伴可以点击如下链接获取,资源地址:技术资料笔记。

不会有人刷到这里还想白嫖吧?点赞对我真的非常重要!在线求赞。加个关注我会非常感激!
多线程系列(十一) -浅析并发读写锁StampedLock的更多相关文章
- 源码分析:升级版的读写锁 StampedLock
简介 StampedLock 是JDK1.8 开始提供的一种锁, 是对之前介绍的读写锁 ReentrantReadWriteLock 的功能增强.StampedLock 有三种模式:Writing(读 ...
- 读写锁StampedLock的思想
该类是一个读写锁的改进,它的思想是读写锁中读不仅不阻塞读,同时也不应该阻塞写. 读不阻塞写的实现思路: 在读的时候如果发生了写,则应当重读而不是在读的时候直接阻塞写! 因为在读线程非常多而写线程比较少 ...
- Java之——redis并发读写锁,使用Redisson实现分布式锁
原文:http://blog.csdn.net/l1028386804/article/details/73523810 1. 可重入锁(Reentrant Lock) Redisson的分布式可重入 ...
- Java 并发 —— 读写锁(ReadWriteLock)
读写锁(ReadWriteLock),顾名思义,就是在读写某文件时,对该文件上锁. 1. ReentrantReadWriteLock 三部曲: 加锁: 读写操作: 解锁:(为保证解锁操作一定执行,通 ...
- Android多线程研究(9)——读写锁
一.什么是锁 在Java的util.concurrent.locks包下有关于锁的接口和类如下: 先看一段代码: package com.codeing.snail.test; public clas ...
- Go语言协程并发---读写锁sync.RWMutex
package main import ( "fmt" "sync" "time" ) /* 读写锁 多路只读 一路只写 读写互斥 */ / ...
- java多线程系列五、并发容器
一.ConcurrentHashMap 1.为什么要使用ConcurrentHashMap 在多线程环境下,使用HashMap进行put操作会引起死循环,导致CPU利用率接近100%,HashMap在 ...
- Golang map并发 读写锁
golang并发 一:只有写操作 var ( count int l = sync.Mutex{} m = make(map[int]int) ) //全局变量并发写 导致计数错误 func vari ...
- Java并发(8)- 读写锁中的性能之王:StampedLock
在上一篇<你真的懂ReentrantReadWriteLock吗?>中我给大家留了一个引子,一个更高效同时可以避免写饥饿的读写锁---StampedLock.StampedLock实现了不 ...
- 正确使用Java读写锁
JDK8中引入了高性能的读写锁StampedLock,它的核心思想在于,在读的时候如果发生了写,应该通过重试的方式来获取新的值,而不应该阻塞写操作.这种模式也就是典型的无锁编程思想,和CAS自旋的思想 ...
随机推荐
- 【解决一个小问题】golang 的 `-race`选项导致 unsafe代码 panic
作者:张富春(ahfuzhang),转载时请注明作者和引用链接,谢谢! cnblogs博客 zhihu Github 公众号:一本正经的瞎扯 为了提升性能,使用 unsafe 代码来重构了凯撒加密的代 ...
- VS2013未能正确加载microsoft.visualstudio.editor.implementation.editorpackage
在用VS2013做项目,但是特别不顺利,这不,VS2013突然罢工了,连解决方案都打不开了,会出现如下的错误: 试了网上的各种解决方案,总算找到一个适合自己的,具体方法如下: 1.打开VS2013的& ...
- C#使用命令行打开diskpart修改盘符
参考链接: https://www.cnblogs.com/k98091518/p/6019296.html https://learn.microsoft.com/zh-cn/windows-ser ...
- 从零开始配置 vim(17)——快捷键提示
之前我们定义了各种各样的快捷键,有为了增强功能自定义的,有针对插件的.数量一多有的时候就不那么容易记忆了.要是每次要去配置文件找我定义了哪些快捷键肯定会影响使用的. 本篇将要介绍一个插件,它是快捷键的 ...
- vim 从嫌弃到依赖(0)——概述
最近我想开一个新的系列,记录我使用vim的相关心得.初次接触vim是在大学操作系统实践课程中,跟着Linux一块进行学习的.当初我是百般嫌弃它的,想要进行编辑还要按下其他键,我想要移动光标居然还的切换 ...
- django 处理请求
本文基于 django runsever 入口 执行 python manage.py runserver 调用 django.core.management.commands.runserver.C ...
- Redis主从配置、数据持久化、集群
发布订阅 ## subscribe 订阅一个或者多个频道 ## publish 给指定的频道发送消息 ## psubscribe 订阅指定模式的频道,*代表所有 ## pubsub channels ...
- 使用Nuget快速集成.Net三维控件
据老一辈的程序员说开发三维程序门槛很高,需要学若干年才能入门,自从遇上AnyCAD三维控件后,开发三维应用变的简单了.当结合nuget后,一切更简单了. 1 准备工作 安装VS201x以后,就可以开始 ...
- 使用DoraCloud构建远程办公桌面云
公司总部在上海.员工分布在各地.部分员工需要远程办公.为了实现远程办公,有几种备选方案. 方案1.在员工的PC上安装向日葵.ToDesk之类的远程工具. 方案2.公司总部提供VPN,员工通过VPN拨号 ...
- Oracle删除索引规范
1.背景概述 2.索引删除规范 3.根本解决方案及建议 1.背景概述 近期应用升级上线过程中,存在删除业务表索引的变更操作,且因删除索引导致次日业务高峰时期,数据库响应缓慢的情况,经定位是缺失索引导致 ...