JavaStampedLock读写锁实现方法
时间:2026-02-04 15:33:48 271浏览 收藏
在IT行业这个发展更新速度很快的行业,只有不停止的学习,才不会被行业所淘汰。如果你是文章学习者,那么本文《Java如何用StampedLock实现读写锁》就很适合你!本篇内容主要包括##content_title##,希望对大家的知识积累有所帮助,助力实战开发!
StampedLock相比ReentrantReadWriteLock的优势在于其支持乐观读,通过tryOptimisticRead和validate机制,在读多写少场景下减少锁竞争,提升性能;适用于配置中心、缓存等高频读低频写场景,但需注意不可重入、无条件变量及降级处理复杂性等问题。

在Java中,StampedLock 提供了一种比 ReentrantReadWriteLock 更灵活、性能更优的读写锁机制,尤其是在读操作远多于写操作的场景下。它通过引入“戳”(stamp)的概念,支持三种模式:写锁(独占)、悲观读锁(共享)和乐观读(非阻塞)。核心在于,它允许读操作在不阻塞写操作的情况下进行,并在读操作完成后验证数据是否被修改过,以此来提升并发性能。
解决方案
使用 StampedLock 实现读写锁的基本思路,无非就是围绕其核心的 writeLock(), readLock() 和 tryOptimisticRead() 方法展开。我通常会这样来构建我的并发访问逻辑:
首先,你需要实例化一个 StampedLock 对象:
import java.util.concurrent.locks.StampedLock;
public class SharedResource {
private double value;
private final StampedLock lock = new StampedLock();
public SharedResource(double initialValue) {
this.value = initialValue;
}
// 写操作:独占访问
public void setValue(double newValue) {
long stamp = lock.writeLock(); // 获取写锁
try {
System.out.println(Thread.currentThread().getName() + " 正在写入数据...");
this.value = newValue;
// 模拟耗时操作
try { Thread.sleep(50); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
System.out.println(Thread.currentThread().getName() + " 数据写入完成,新值: " + newValue);
} finally {
lock.unlockWrite(stamp); // 释放写锁
}
}
// 读操作:优先尝试乐观读,失败则降级为悲观读
public double getValue() {
long stamp = lock.tryOptimisticRead(); // 尝试获取乐观读戳
double currentValue = value; // 读取数据
// 验证乐观读期间是否有写操作发生
if (!lock.validate(stamp)) {
// 乐观读失败,说明数据可能已被修改,降级为悲观读
stamp = lock.readLock(); // 获取悲观读锁
try {
System.out.println(Thread.currentThread().getName() + " 乐观读失败,降级为悲观读...");
currentValue = value; // 重新读取数据
// 模拟耗时操作
try { Thread.sleep(10); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
System.out.println(Thread.currentThread().getName() + " 悲观读完成,值: " + currentValue);
} finally {
lock.unlockRead(stamp); // 释放悲观读锁
}
} else {
// 乐观读成功
System.out.println(Thread.currentThread().getName() + " 乐观读成功,值: " + currentValue);
// 模拟耗时操作
try { Thread.sleep(10); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
return currentValue;
}
public static void main(String[] args) {
SharedResource resource = new SharedResource(0.0);
// 启动多个读线程
for (int i = 0; i < 5; i++) {
new Thread(() -> {
for (int j = 0; j < 3; j++) {
resource.getValue();
try { Thread.sleep((long) (Math.random() * 100)); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
}, "Reader-" + i).start();
}
// 启动一个写线程
new Thread(() -> {
for (int i = 0; i < 2; i++) {
double newValue = Math.random() * 100;
resource.setValue(newValue);
try { Thread.sleep((long) (Math.random() * 200)); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
}, "Writer-1").start();
}
}这段代码展示了 StampedLock 最常见的用法:写操作是独占的,而读操作则会优先尝试乐观读。如果乐观读在验证时发现数据已被修改,它会“回退”到悲观读模式,重新获取锁并读取数据。这在读多写少的场景下,能显著减少锁竞争,提高系统吞吐量。
StampedLock与ReentrantReadWriteLock相比,有哪些显著优势和使用场景?
当我第一次接触到 StampedLock 时,我其实有点疑惑,Java 已经有了 ReentrantReadWriteLock,为什么还需要一个新的读写锁?但深入了解后,我发现 StampedLock 确实带来了不少创新点,尤其是在性能优化上。
它最显著的优势,无疑是其引入的乐观读(Optimistic Read)模式。ReentrantReadWriteLock 的读锁是悲观的,只要有读锁存在,写锁就无法获取,反之亦然。但在很多应用中,数据被修改的频率远低于被读取的频率。StampedLock 的乐观读允许读操作在不获取任何锁(或者说,获取一个“零成本”的逻辑锁)的情况下进行。它只是在读操作开始时记录一个“戳”,在读完数据后,再验证这个戳是否有效。如果戳有效,说明期间没有写操作发生,读到的数据是“一致的”;如果戳无效,才需要回退到悲观读模式重新读取。这种机制极大地减少了读操作的开销,降低了锁的粒度,从而提升了并发性能。
另一个微妙但重要的点是,StampedLock 的设计在某些方面能更好地利用现代CPU的缓存一致性协议。乐观读在读取共享变量时,通常会直接从CPU缓存中获取,而不需要像悲观锁那样频繁地刷新缓存或进行昂贵的总线操作来同步内存。
当然,优势背后也伴随着一些权衡。StampedLock 的API比 ReentrantReadWriteLock 更复杂,使用起来需要更小心。它不支持条件变量,也不具备可重入性(这点与 ReentrantReadWriteLock 形成鲜明对比,后者是可重入的)。这意味着如果你在一个持有 StampedLock 读锁的方法内部,再次尝试获取读锁,可能会导致死锁或者意外行为。
使用场景上,StampedLock 最适合那些读操作远多于写操作,且对性能要求极高的场景。例如,一个配置中心,配置数据一旦加载后很少变动,但会被大量服务频繁读取;或者一个缓存系统,缓存数据更新不频繁,但查询量巨大。在这种场景下,乐观读能发挥最大的价值,让大部分读请求几乎无锁运行。如果你的应用读写比例接近,或者写操作非常频繁,那么 ReentrantReadWriteLock 可能因为其简单性和可重入性,依然是更稳妥的选择。
深入理解StampedLock的乐观读(Optimistic Read)机制及其潜在的“坑”?
乐观读,这玩意儿听起来就很美好,对吧?“乐观”这个词本身就带着一种积极向上的感觉。但计算机世界里的“乐观”,往往意味着你需要为它的“不乐观”做好准备。
StampedLock 的乐观读机制,核心在于 tryOptimisticRead() 和 validate(long stamp) 这两个方法。当你调用 tryOptimisticRead() 时,它会返回一个非零的 stamp 值,表示当前锁的状态。此时,你就可以去读取共享数据了。重点来了:在 tryOptimisticRead() 和 validate(stamp) 之间,任何写操作都不会被阻塞,也不会阻塞你的读操作。这意味着,你的读操作可能在读取过程中,数据就被其他线程修改了。
这就是为什么在读取完数据后,你必须调用 validate(stamp) 来验证这个 stamp 是否仍然有效。如果 validate() 返回 true,恭喜你,在你的读操作期间,没有写操作发生,你读到的数据是“一致的”。如果返回 false,那就意味着在你读取数据期间,有写操作成功获取了写锁并修改了数据。这时候,你之前读取到的数据就可能是“脏数据”或“不一致数据”了。
潜在的“坑”和注意事项:
数据不一致性风险:这是最直接的坑。在
tryOptimisticRead()到validate()之间,你读取到的数据,不能直接用于业务逻辑判断或计算,因为它可能是过时的。你必须在validate()成功后才能信任这些数据。如果你在验证前就使用了数据,那么你的程序可能会基于错误的信息做出决策。long stamp = lock.tryOptimisticRead(); // 假设这里读取了多个字段 double x = this.x; double y = this.y; if (!lock.validate(stamp)) { // 乐观读失败,x和y可能不一致,需要重新获取悲观读锁并读取 // ... 降级处理 ... } else { // 乐观读成功,x和y一致且有效 // ... 使用 x 和 y ... }降级处理的复杂性:当
validate()失败时,你通常需要“降级”到悲观读模式,也就是调用readLock()获取一个悲观读锁,然后重新读取数据。这个降级过程会增加代码的复杂性,你需要确保降级逻辑是健壮的,能够正确处理所有情况。如果降级逻辑本身有缺陷,可能会引入新的bug。不能在乐观读模式下修改数据:乐观读只是为了读取,如果你在乐观读期间尝试修改共享数据,那简直就是自找麻烦,因为乐观读不提供任何互斥保证。
可见性问题:虽然
StampedLock在内部会处理内存屏障,但乐观读本身不保证你读取到的数据是最新的写入。它只是通过validate()告诉你,在你读取期间是否有写操作。如果validate()失败,你需要重新读取。循环重试的潜在风险:在某些极端情况下,如果写操作非常频繁,乐观读可能会频繁失败,导致线程不断地进行乐观读 -> 失败 -> 悲观读 -> 释放 -> 再次尝试乐观读的循环,这可能会带来额外的开销,甚至影响性能。因此,设计时要考虑这种重试的成本。
总之,乐观读是一种“赌博”:赌在你看数据的时候,数据不会变。如果赌赢了,性能飙升;如果赌输了,就老老实实地回退到悲观模式。理解它的工作原理和局限性,是正确使用它的关键。
如何在实际项目中正确地管理StampedLock的锁升级与降级,以避免死锁或性能瓶颈?
StampedLock 的锁升级和降级机制,是它比 ReentrantReadWriteLock 更灵活但也更复杂的地方。管理不当,确实容易引入死锁或者不必要的性能开销。
1. 锁升级:从乐观读到悲观读
这其实就是我们上面示例中展示的场景。当你尝试乐观读,但 validate() 失败时,你就需要将读模式从“乐观”升级为“悲观”。
long stamp = lock.tryOptimisticRead();
// 读取数据...
if (!lock.validate(stamp)) {
// 乐观读失败,升级为悲观读
stamp = lock.readLock(); // 获取悲观读锁
try {
// 重新读取数据,此时数据是受保护且一致的
// ...
} finally {
lock.unlockRead(stamp); // 释放悲观读锁
}
}
// 乐观读成功或悲观读完成,继续业务逻辑这里需要注意的是,StampedLock 的读锁是不可重入的。如果你在一个已经持有读锁的线程中再次尝试获取读锁,它会阻塞并可能导致死锁。所以,一旦你获取了悲观读锁,就不要在 finally 块释放它之前再次尝试获取。
2. 锁降级:从写锁到读锁
这是一种优化策略,当你完成了对数据的修改,但后续还需要基于修改后的数据进行一系列读取操作时,可以考虑将写锁降级为读锁。这样可以避免完全释放写锁再重新获取读锁的开销,并且允许其他读线程并发访问。
StampedLock 提供了 tryConvertToReadLock(long stamp) 方法来实现这种降级。
long stamp = lock.writeLock(); // 获取写锁
try {
// 执行写操作...
this.value = newValue;
// 尝试降级为读锁
long newStamp = lock.tryConvertToReadLock(stamp);
if (newStamp != 0L) { // 降级成功
stamp = newStamp; // 更新stamp,现在持有的是读锁
// 可以在这里执行基于新值的读操作
System.out.println("写锁降级为读锁成功,当前值为: " + this.value);
} else { // 降级失败,可能期间有其他写操作竞争,或者其他原因
System.out.println("写锁降级为读锁失败,继续持有写锁或完全释放写锁再获取读锁");
// 如果降级失败,你仍然持有写锁。根据业务逻辑决定是继续持有写锁,
// 还是释放写锁,然后通过 readLock() 重新获取读锁。
// 为了避免死锁,通常会先释放写锁,再获取读锁。
lock.unlockWrite(stamp); // 释放写锁
stamp = lock.readLock(); // 获取读锁
try {
System.out.println("重新获取读锁成功,当前值为: " + this.value);
} finally {
lock.unlockRead(stamp);
}
return; // 或者根据业务逻辑处理
}
// 如果降级成功,这里是读锁模式
// 执行读操作...
double readVal = this.value;
// ...
} finally {
// 无论降级成功与否,最终都要释放当前持有的锁
if (StampedLock.isReadLockStamp(stamp)) { // 判断当前持有的是否是读锁
lock.unlockRead(stamp);
} else if (StampedLock.isWriteLockStamp(stamp)) { // 判断当前持有的是否是写锁
lock.unlockWrite(stamp);
}
// 注意:StampedLock的stamp是long类型,不能直接判断是否为0,需要用isReadLockStamp/isWriteLockStamp
}避免死锁和性能瓶颈的关键点:
try-finally块:无论何时获取锁,都必须在try块中执行受保护的操作,并在finally块中释放锁。这是防止死锁和资源泄露的基本准则。- 不可重入性:
StampedLock的读锁和写锁都是不可重入的。这意味着在一个已经持有读锁的线程中,再次尝试获取读锁会阻塞;在一个持有写锁的线程中,再次尝试获取写锁也会阻塞。这是与ReentrantReadWriteLock的一个重要区别。在设计代码时要时刻牢记这一点,避免嵌套的锁获取操作。 - 锁粒度:尽量缩小锁的范围,只在真正需要保护共享资源的地方加锁。过大的锁范围会降低并发性。
- 降级失败处理:当
tryConvertToReadLock()失败时,你仍然持有写锁。此时,你需要根据业务逻辑决定是继续持有写锁,还是先释放写锁再获取读锁。直接在持有写锁的情况下尝试获取读锁(如果tryConvertToReadLock失败,你仍持有写锁)是安全的,因为降级操作本身就是从写锁到读锁的转换。但如果业务逻辑需要完全释放再获取,则需小心处理。 - 乐观读的重试策略:如果乐观读频繁失败,考虑是否需要调整策略,比如增加
Thread.yield()或短暂sleep,或者直接切换到悲观读模式,以避免CPU空转和无谓的重试开销。
StampedLock 提供了强大的并发控制能力,但它的复杂性也要求开发者有更深入的理解和更严谨的代码设计。正确地运用它的锁升级与降级机制,可以显著提升高并发场景下的应用性能。
终于介绍完啦!小伙伴们,这篇关于《JavaStampedLock读写锁实现方法》的介绍应该让你收获多多了吧!欢迎大家收藏或分享给更多需要学习的朋友吧~golang学习网公众号也会发布文章相关知识,快来关注吧!
-
501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
130 收藏
-
275 收藏
-
501 收藏
-
363 收藏
-
357 收藏
-
382 收藏
-
411 收藏
-
177 收藏
-
202 收藏
-
109 收藏
-
260 收藏
-
441 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习