当前位置: 首页 > news >正文

并发编程原理与实战(二十三)StampedLock应用实战与其他锁性能对比分析

上一篇我们学习了StampedLock邮戳锁引入背景以及邮戳锁的三种锁模式、邮戳锁的特性以及邮戳锁的核心api。本来就来进一步学习邮戳锁的具体使用。

首先我们先来分析官方提供的实例,分析StampedLock的主要api方法的使用方法。官方提供的例子是多线程环境下更新点的坐标并计算到原点的距离,这个功能在日常的业务开发中并不是很常见,但不妨碍我们对相关api使用的了解。

多线程更新坐标并计算到原点的距离

核心需求分析

1、线程安全的更新坐标值

  • 写操作需独占锁保证原子性,防止并发修改导致数据错乱
  • 使用writeLock()获取写锁,更新后通过unlockWrite(stamp)释放写锁。

2、高效的距离计算

  • 优先采用乐观读(tryOptimisticRead)减少锁竞争,以便适合高频读取的场景。
  • 若检测到写冲突(validate(stamp)失败),降级为悲观读锁重新获取数据。

3、数据一致性保证

  • 乐观读需拷贝共享变量到局部变量,避免验证后数据被修改。
  • 悲观读锁与写锁互斥,确保强一致性但可能降低吞吐量。

代码实现

我们对官方提供的例子稍微做了改造和备注,如下:

public class Point {// 坐标点private static double x, y;// 创建邮戳锁private static final StampedLock sl = new StampedLock();// 一个独占锁方法(写锁是独占的)public static void move(double deltaX, double deltaY) {// 获取写锁long stamp = sl.writeLock();try {// 修改坐标x += deltaX;y += deltaY;System.out.printf("Thread %s moved to (%.1f, %.1f)%n", Thread.currentThread().getName(), x, y);} finally {// 根据邮戳释放写锁sl.unlockWrite(stamp);}}// 一个只读的方法// 从乐观读锁升级为读锁(悲观读)public static double distanceFromOrigin() {// 尝试乐观读锁,返回非0的stamp表示当前无写锁long stamp = sl.tryOptimisticRead();try {retryHoldingLock:// 获取共享读锁for (; ; stamp = sl.readLock()) {// 存在写锁则重试获取写锁if (stamp == 0L)continue retryHoldingLock;// 尽可能单纯的读操作double currentX = x;double currentY = y;//检查乐观读期间是否有写操作,返回true表示数据有效,若返回false,需升级为悲观读或重试if (!sl.validate(stamp))continue retryHoldingLock;//计算二维平面上点 (x, y) 到原点 (0, 0)double distance = Math.hypot(currentX, currentY);System.out.printf("Thread %s calculated distance: %.2f%n", Thread.currentThread().getName(), distance);return distance;}} finally {//判断邮戳代表的是否是读锁//if (StampedLock.isReadLockStamp(stamp))if (isReadLockStamp(stamp))//释放读锁sl.unlockRead(stamp);}}// 乐观读锁升级为写锁public static void moveIfAtOrigin(double newX, double newY) {long stamp = sl.tryOptimisticRead();try {retryHoldingLock:for (; ; stamp = sl.writeLock()) {if (stamp == 0L)continue retryHoldingLock;double currentX = x;double currentY = y;if (!sl.validate(stamp))continue retryHoldingLock;if (currentX != 0.0 || currentY != 0.0)break;//尝试将读锁升级为写锁,返回新stamp或0。stamp = sl.tryConvertToWriteLock(stamp);if (stamp == 0L)//升级失败重新获取写锁。continue retryHoldingLock;// 排他访问x = newX;y = newY;return;}} finally {//判断邮戳代表的是否是写锁//if (StampedLock.isWriteLockStamp(stamp))if (!isReadLockStamp(stamp))//释放写锁sl.unlockWrite(stamp);}}// 读锁升级为写锁public static void moveIfAtOrigin2(double newX, double newY) {long stamp = sl.readLock();try {while (x == 0.0 && y == 0.0) {//尝试将读锁升级为写锁,返回新stamp或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);}}/**判断是否是读锁**/public static boolean isReadLockStamp(long stamp) {return (stamp & 0xFFL) != 0L;}
}

读写线程比例设计

例子中并没有提供怎么使用多线程来读写坐标的方法,所以我们来补充这个多线程读写的方法。既然StampedLock适合在读多写少的场景中使用,那么读线程要比写线程多,以便模拟高频读和低频写操作的场景,且写线程间隔时间应明显大于读线程(如5秒 vs 300ms)以观察锁竞争情况。基于上述分析,我们增加一个main函数来测试下上述代码。

public static void main(String[] args) {//创建1个写线程new Thread(() -> {for (; ; ) {move(2.0, 2.0);try {//5秒写一次坐标Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();}}}).start();//创建9个读线程for (int i = 0; i < 9; i++) {new Thread(() -> {for (; ; ) {distanceFromOrigin();try {//300毫秒秒读一次坐标Thread.sleep(300);} catch (InterruptedException e) {e.printStackTrace();}}}).start();}
}

main函数中,我们创建一个线程不断的移动坐标,每5秒执行一次移动坐标操作(写操作);创建9个读线程不断读坐标值并计算到圆点的距离,每300毫秒秒读一次坐标。观察下运行结果:

Thread Thread-0 moved to (2.0, 2.0)
Thread Thread-2 calculated distance: 2.83
Thread Thread-4 calculated distance: 2.83
Thread Thread-5 calculated distance: 2.83
Thread Thread-7 calculated distance: 2.83
Thread Thread-6 calculated distance: 2.83
Thread Thread-8 calculated distance: 2.83
Thread Thread-1 calculated distance: 2.83
Thread Thread-3 calculated distance: 2.83
Thread Thread-9 calculated distance: 2.83
...
Thread Thread-0 moved to (4.0, 4.0)
Thread Thread-8 calculated distance: 5.66
Thread Thread-7 calculated distance: 5.66
Thread Thread-5 calculated distance: 5.66
Thread Thread-3 calculated distance: 5.66
Thread Thread-1 calculated distance: 5.66
Thread Thread-2 calculated distance: 5.66
Thread Thread-4 calculated distance: 5.66
Thread Thread-9 calculated distance: 5.66
...

从运行结果可以看出,写操作和读操作是互斥的,数据没有任何错乱。

乐观读性能分析

乐观锁实现

那问题来了,这个例子中怎么体现使用StampedLock的乐观读后带来的性能提升?所以我们得用之前已经掌握的锁来读操作来进行对比。首先先对distanceFromOrigin()方法进行改造,增加读取耗时打印,如下:

// 一个只读的方法
// 从乐观读锁升级为读锁(悲观读)
public static double distanceFromOrigin() {long startTime = System.currentTimeMillis();// 尝试乐观读锁,返回非0的stamp表示当前无写锁long stamp = sl.tryOptimisticRead();double distance = 0;try {retryHoldingLock:// 获取共享读锁for (; ; stamp = sl.readLock()) {// 存在写锁则重试获取写锁if (stamp == 0L)continue retryHoldingLock;// 尽可能单纯的读操作double currentX = x;double currentY = y;//检查乐观读期间是否有写操作,返回true表示数据有效,若返回false,需升级为悲观读或重试if (!sl.validate(stamp))continue retryHoldingLock;//计算二维平面上点 (x, y) 到原点 (0, 0)distance = Math.hypot(currentX, currentY);System.out.printf("Thread %s calculated distance: %.2f%n", Thread.currentThread().getName(), distance);break;}} finally {//判断邮戳代表的是否是读锁//if (StampedLock.isReadLockStamp(stamp))if (isReadLockStamp(stamp))//释放读锁sl.unlockRead(stamp);long endTime = System.currentTimeMillis();System.out.println("Thread "+Thread.currentThread().getName()+" calculated distance cost time: "+(endTime-startTime));return distance;}
}

运行结果如下:

Thread Thread-1 calculated distance: 0.00
Thread Thread-0 moved to (2.0, 2.0)
Thread Thread-1 calculated distance cost time: 37
Thread Thread-2 calculated distance: 2.83
Thread Thread-2 calculated distance cost time: 29
Thread Thread-9 calculated distance: 2.83
Thread Thread-8 calculated distance: 2.83
Thread Thread-8 calculated distance cost time: 26
Thread Thread-7 calculated distance: 2.83
Thread Thread-7 calculated distance cost time: 26
Thread Thread-6 calculated distance: 2.83
Thread Thread-6 calculated distance cost time: 27
Thread Thread-5 calculated distance: 2.83
Thread Thread-5 calculated distance cost time: 27
Thread Thread-4 calculated distance: 2.83
Thread Thread-4 calculated distance cost time: 30
Thread Thread-3 calculated distance: 2.83
Thread Thread-3 calculated distance cost time: 31
Thread Thread-9 calculated distance cost time: 25

独占锁synchronized实现

下面使用synchronized对读取坐标过程加锁,代码如下:

public static double distanceFromOrigin2() {long startTime = System.currentTimeMillis();double distance = 0;synchronized (Point.class) {// 尽可能单纯的读操作double currentX = x;double currentY = y;distance = Math.hypot(currentX, currentY);System.out.printf("Thread %s calculated distance: %.2f%n", Thread.currentThread().getName(), distance);}long endTime = System.currentTimeMillis();System.out.println("Thread " + Thread.currentThread().getName() + " calculated distance cost time: " + (endTime - startTime));return distance;
}

main方法中读线程改成调用该方法

//创建10个读线程
for (int i = 0; i < 9; i++) {new Thread(() -> {for (; ; ) {//distanceFromOrigin();distanceFromOrigin2();try {//300毫秒秒写一次坐标Thread.sleep(300);} catch (InterruptedException e) {e.printStackTrace();}}}).start();
}

看看运行结果

Thread Thread-0 moved to (2.0, 2.0)
Thread Thread-2 calculated distance: 2.83
Thread Thread-2 calculated distance cost time: 31
Thread Thread-9 calculated distance: 2.83
Thread Thread-9 calculated distance cost time: 8
Thread Thread-8 calculated distance: 2.83
Thread Thread-8 calculated distance cost time: 9
Thread Thread-7 calculated distance: 2.83
Thread Thread-7 calculated distance cost time: 14
Thread Thread-6 calculated distance: 2.83
Thread Thread-6 calculated distance cost time: 31
Thread Thread-5 calculated distance: 2.83
Thread Thread-5 calculated distance cost time: 32
Thread Thread-4 calculated distance: 2.83
Thread Thread-4 calculated distance cost time: 33
Thread Thread-3 calculated distance: 2.83
Thread Thread-3 calculated distance cost time: 34
Thread Thread-1 calculated distance: 2.83
Thread Thread-1 calculated distance cost time: 34

从运行结果看,synchronized的性能视乎更好,可能因为synchronized内部已经做了优化的结果。我们换成ReentrantLock看看。

// 一个只读的方法
// ReentrantLock实现
public static double distanceFromOrigin3() {long startTime = System.currentTimeMillis();double distance = 0;lockObject.lock();try {// 尽可能单纯的读操作double currentX = x;double currentY = y;distance = Math.hypot(currentX, currentY);} finally {lockObject.unlock();}System.out.printf("Thread %s calculated distance: %.2f%n", Thread.currentThread().getName(), distance);long endTime = System.currentTimeMillis();System.out.println("Thread " + Thread.currentThread().getName() + " calculated distance cost time: " + (endTime - startTime));return distance;
}

运行结果

Thread Thread-0 moved to (2.0, 2.0)
Thread Thread-1 calculated distance: 2.83
Thread Thread-2 calculated distance: 2.83
Thread Thread-2 calculated distance cost time: 38
Thread Thread-3 calculated distance: 2.83
Thread Thread-3 calculated distance cost time: 34
Thread Thread-4 calculated distance: 2.83
Thread Thread-4 calculated distance cost time: 35
Thread Thread-5 calculated distance: 2.83
Thread Thread-5 calculated distance cost time: 35
Thread Thread-6 calculated distance: 2.83
Thread Thread-6 calculated distance cost time: 31
Thread Thread-7 calculated distance: 2.83
Thread Thread-7 calculated distance cost time: 32
Thread Thread-8 calculated distance: 2.83
Thread Thread-8 calculated distance cost time: 31
Thread Thread-9 calculated distance: 2.83
Thread Thread-9 calculated distance cost time: 31
Thread Thread-1 calculated distance cost time: 39

换成ReentrantLock后,从运行结果可以看出,耗时明细长了,都在30毫秒以上,这从侧面印证乐观锁的性能确实比ReentrantLock好。

总结

本文通过官方提供的多线程更新坐标并计算到原点的距离这个例子,说明了StampedLock的应用,然后通过设计不同的读写线程,分析读写坐标时的数据一致性,最后通过用ReentrantLock和synchronized分别对读取坐标进行加锁,对比了与StampedLock 乐观锁实现的耗时。通过该例子,进一步加深了对StampedLock 的理解。

http://www.lryc.cn/news/620954.html

相关文章:

  • 深度学习ubuntu系统常用指令和技巧
  • VisDrone数据集,专为无人机视觉任务打造
  • Linux面试题及详细答案 120道(1-15)-- 基础概念
  • 9.【C++进阶】继承
  • 开源数据发现平台:Amundsen 快速上手指南
  • 微服务、分布式概念-以及集群部署 vs 分布式部署
  • C# LINQ 全面教程:从入门到精通
  • 【19-模型训练细节 】
  • Linux 编译过程中遇到 TMPDIR 空间不足的问题
  • 算法应用上新!自适应更新策略差分进化算法求解球形多飞行器路径规划问题,附完整MATLAB代码
  • 智慧城市SaaS平台/专项管理系统
  • PyCharm 2025.2:面向工程师的 AI 工具
  • Nginx学习笔记(九)—— Nginx Rewrite深度解析
  • 学习嵌入式第二十八天
  • python爬虫学习(2)
  • 大模型微调方法讲解
  • linux 软硬链接详解
  • 服务器数据恢复—误删服务器卷数据的数据恢复案例
  • ESXI 6.7服务器时间错乱问题
  • QT+Yolov8 推理部署,ONNX模型 ,实例分割+目标检测
  • 【会员专享数据】2000-2024年我国乡镇的逐日PM₁₀数据(Shp/Excel格式)
  • 6、C 语言指针初阶知识点总结
  • AI搜索优化专家孟庆涛:以技术温度重构“人机信息对话”新范式
  • 前端Vite介绍(现代化前端构建工具,由尤雨溪开发,旨在显著提升开发体验和构建效率)ES模块(ESM)、与传统Webpack对比、Rollup打包
  • 飞算JavaAI合并项目实战:7天完成3年遗留系统重构
  • 92、23种设计模式-单例模式
  • LeetCode 面试经典 150_数组/字符串_最后一个单词的长度(19_58_C++_简单)(反向遍历)
  • vector 认识及使用
  • MTK平台Wi-Fi学习--如何修改wifi 的TX Power
  • 计算机毕设大数据选题推荐 基于spark+Hadoop+python的贵州茅台股票数据分析系统【源码+文档+调试】