StampedLock 的乐观读机制主要解决了读多写少场景下,传统读写锁(如 ReentrantReadWriteLock)可能存在的写线程饥饿或性能瓶颈问题。它通过一种“乐观”的策略,允许读操作在特定条件下完全不阻塞写操作,从而显著提高系统的整体吞吐量。

解决的问题

  1. 写线程饥饿:

    • 在传统的读写锁(ReentrantReadWriteLock)中,读锁是共享的。当有大量读线程持续持有读锁时,写线程可能长时间无法获取写锁(因为写锁需要独占访问),导致写操作被“饿死”。
  2. 悲观锁的开销:
    • 即使没有写操作正在进行,传统的读锁在获取和释放时仍然需要一定的同步开销(CAS操作、维护内部状态等)。在超高并发的读场景下,这些开销累积起来可能成为性能瓶颈。
  3. 读操作占主导:
    • 在大多数应用场景(如缓存、配置读取)中,读操作的频率远高于写操作。传统读写锁的设计(读优先或公平策略)在这种场景下效率不高。

乐观读机制的核心思想

  1. “乐观”假设: 假设在读操作进行期间,很可能没有写操作发生
  2. 不阻塞写: 乐观读不获取真正的锁,因此它完全不会阻塞试图获取写锁的线程。写线程总是可以立即尝试获取写锁。
  3. 版本验证: 在乐观读完成之后、使用读取到的数据之前,必须验证在读操作期间是否发生过写操作。这是通过检查一个“戳记”来实现的。
  4. 失败降级: 如果验证失败(表明在读操作期间发生了写操作),则乐观读的结果可能无效。此时,调用者可以选择重试、放弃或者降级为获取一个悲观的读锁(会阻塞写)来确保读取到一致的数据。

如何使用乐观读

使用 StampedLock 的乐观读遵循以下模式:

import java.util.concurrent.locks.StampedLock;

public class OptimisticReadingExample {
private final StampedLock lock = new StampedLock();
private double x, y; // 共享数据 // 计算距离的方法 (使用乐观读)
public double distanceFromOrigin() {
// 1. 尝试乐观读:获取一个戳记(stamp)
long stamp = lock.tryOptimisticRead(); // 2. 将共享变量读入本地局部变量(此时数据可能被其他线程修改!)
double currentX = x;
double currentY = y; // 3. 关键:验证戳记是否仍然有效(自获取戳记以来是否有过写操作?)
if (!lock.validate(stamp)) {
// 3a. 验证失败:乐观读期间发生了写操作!戳记已失效。
// 降级为获取悲观的读锁(会阻塞写,确保读取一致性)
stamp = lock.readLock(); // 这是一个阻塞调用
try {
// 在悲观读锁保护下重新读取数据
currentX = x;
currentY = y;
} finally {
// 无论如何都要释放读锁
lock.unlockRead(stamp);
}
}
// 3b. 如果验证成功,或者降级后重新读取成功,则使用 currentX 和 currentY 进行计算
return Math.sqrt(currentX * currentX + currentY * currentY);
} // 写操作方法
public void move(double deltaX, double deltaY) {
long stamp = lock.writeLock(); // 获取独占写锁
try {
x += deltaX;
y += deltaY;
} finally {
lock.unlockWrite(stamp); // 释放写锁
}
}
}

关键步骤详解

  1. long stamp = lock.tryOptimisticRead();

    • 尝试获取一个乐观读戳记 (stamp)。这个方法非常快,通常只是一个内存读取或简单的原子操作,不涉及锁竞争,不会阻塞任何线程(包括写线程)
    • 这个 stamp 代表了数据当前的一个“版本”或“状态”。如果之后没有写操作发生,这个版本应该保持不变。
  2. 读取共享数据到局部变量 (currentX = x; currentY = y;)

    • 将你需要访问的共享数据复制到方法的局部变量中。这是必须的,因为后续的验证只保证到验证那一刻为止的状态,如果验证成功后你又去直接读 xy,数据可能又被修改了。
    • 重要: 在这个读取过程中,没有任何锁阻止其他线程(尤其是写线程)修改 xy!所以此时读取到的 currentXcurrentY 可能是不一致的(例如,x 被修改了但 y 还没改),或者过时的。
  3. if (!lock.validate(stamp)) { ... }

    • 这是最核心的步骤。调用 validate(stamp) 检查自你获取乐观读戳记 (stamp) 以来,是否有任何线程成功获取了写锁并修改了数据。
    • 如果返回 true (验证成功):
      • 意味着在你获取 stamp 之后,直到 validate 调用执行的那一刻,没有发生过写操作。你可以确信第 2 步读取到的 currentXcurrentY一致有效的(至少是某个一致状态下的快照)。你可以安全地使用它们进行计算 (return Math.sqrt(...))。
    • 如果返回 false (验证失败):
      • 意味着在你读取数据(第 2 步)的过程中或之后,在 validate 调用之前,至少发生了一次成功的写操作。你第 2 步读取的数据 currentXcurrentY 可能无效(不一致或过时),绝对不能使用它们!
      • 此时,你需要降级 (downgrade) 为传统的、悲观的策略:获取一个读锁 (stamp = lock.readLock();)。这个调用可能会阻塞,等待当前可能存在的写锁释放。
      • 在获取到读锁后,重新读取共享数据 (xy) 到局部变量 (currentX, currentY)。因为现在持有读锁,所以能保证在读取过程中不会有写操作发生,读取到的数据是一致的。
      • finally 块中释放读锁 (lock.unlockRead(stamp);)。
      • 最后,使用在悲观读锁保护下读取到的、一致的数据进行计算。
  4. 使用数据 (return Math.sqrt(...))

    • 无论是乐观读验证成功,还是降级到悲观读后成功读取,最终都使用局部变量 currentXcurrentY 进行计算并返回结果。这些局部变量要么代表一个验证通过的快照(乐观成功),要么代表在悲观读锁保护下获取的最新一致状态(乐观失败后降级)。

使用乐观读的注意事项

  1. 验证 (validate) 是必须的: 绝对不能在未验证或验证失败的情况下使用乐观读读取的数据。
  2. 数据拷贝到局部变量: 必须在获取乐观读戳记后、验证之前,将共享数据复制到方法内部的局部变量。验证通过后只使用这些局部变量。
  3. 乐观读适合短小的读操作: 乐观读操作本身(从获取戳记到验证)应该尽可能短。如果读操作本身耗时很长,那么在此期间发生写操作的概率就非常大,导致验证失败的概率很高,最终可能还是需要降级为悲观读锁,反而失去了性能优势,甚至更慢。
  4. 乐观读不修改共享状态: 乐观读只适用于只读操作。如果你需要在读操作中修改状态,必须使用写锁或其它同步机制。
  5. 乐观读不是锁: tryOptimisticRead() 返回的只是一个戳记 (stamp),不是锁对象。你不能用它来解锁。
  6. StampedLock 不可重入: 同一个线程试图重复获取锁(即使是读锁)会导致死锁。
  7. 没有条件变量: StampedLock 不直接支持 Condition,而 ReentrantReadWriteLock 的写锁可以。
  8. 小心转换: StampedLock 提供了 tryConvertToWriteLock 等方法,但使用它们需要非常小心,容易出错。

总结

StampedLock 的乐观读机制通过牺牲“读操作总是能看到最新数据”的绝对保证(通过后续验证和可能的降级来补偿),换取了在读多写少场景下的超高读并发性能避免写线程饥饿。其核心在于 tryOptimisticRead() 获取戳记、将数据拷贝到局部变量、validate(stamp) 验证、验证失败则降级获取悲观读锁重新读取这一套流程。正确使用乐观读可以显著提升高并发读取场景的系统吞吐量,但必须严格遵守使用模式并理解其注意事项。

【乐观锁实现】StampedLock 的乐观读机制的更多相关文章

  1. 【mybatis-plus】什么是乐观锁?如何实现“乐观锁”

    "乐观锁"这个词以前我也没听过.上次在测试需求的时候,查询数据库发现有一个version字段,于是请教开发这个字干嘛使, 人家回复我:乐观锁,解决并发更新用的.当时大家都忙,咱也不 ...

  2. Elasticsearch由浅入深(五)_version乐观锁、external version乐观锁、partial update、groovy脚本实现partial update

    基于_version进行乐观锁并发控制 先构造一条数据出来 PUT /test_index/test_type/ { "test_field": "test test&q ...

  3. 通俗易懂 悲观锁、乐观锁、可重入锁、自旋锁、偏向锁、轻量/重量级锁、读写锁、各种锁及其Java实现!

    网上关于Java中锁的话题可以说资料相当丰富,但相关内容总感觉是一大串术语的罗列,让人云里雾里,读完就忘.本文希望能为Java新人做一篇通俗易懂的整合,旨在消除对各种各样锁的术语的恐惧感,对每种锁的底 ...

  4. mysql中的锁机制之悲观锁和乐观锁

    1.悲观锁? 悲观锁顾名思义就是很悲观,悲观锁认为数据随时就有可能会被外界进行修改,所以悲观锁一上来就会把数据给加上锁.悲观锁一般都是依靠关系型数据库提供的锁机制,然而事实上关系型数据库中的行锁,表锁 ...

  5. 写文章 通俗易懂 悲观锁、乐观锁、可重入锁、自旋锁、偏向锁、轻量/重量级锁、读写锁、各种锁及其Java实现!

    网上关于Java中锁的话题可以说资料相当丰富,但相关内容总感觉是一大串术语的罗列,让人云里雾里,读完就忘.本文希望能为Java新人做一篇通俗易懂的整合,旨在消除对各种各样锁的术语的恐惧感,对每种锁的底 ...

  6. Mysql锁机制--悲观锁和乐观锁

    1. 悲观锁简介 悲观锁(Pessimistic Concurrency Control,缩写PCC),它指的是对数据被外界修改持保守态度,因此,在整个数据处理过程中, 将数据处于锁定状态.悲观锁的实 ...

  7. InnoDB-MVCC与乐观锁

    最近通过<高性能MySQL>一书学习MySQL方面的知识,在看到书中所讲InnoDB-MVCC部分的时候,有一种强烈的感觉,这不就是乐观锁吗(入门级小学徒的疑惑脸)?当下便去网上以各种方式 ...

  8. 悲观锁,乐观锁,排他锁,行锁----MYSQL

    在说具体的锁结构时,先思考一个问题,那就是为什么要上锁?然后我要如何选择锁?锁具体如何实现? 在文章得末尾我给出了我的个人答案. 一.什么是悲观锁? 1.悲观锁就是在操作数据时,认为此操作会出现数据冲 ...

  9. 基于redis的乐观锁实践

    redis真是一个分布式应用场景下的好东西,对于我们的应用设计,功劳大大的! 今天要研究的是基于redis的事务机制以及watch指令(CAS)实现乐观锁的过程. 所谓乐观锁,就是利用版本号比较机制, ...

  10. hibernate 悲观锁乐观锁

    悲观锁和乐观锁是:在事务隔离机制中设置了ReadCommited的情况下,两种可以避免不可重复读的方式.   设置成读已提交是考虑到安全和处理速度,保证并发效率,但是在这个情况下仍然需要避免不可重复读 ...

随机推荐

  1. UT

    Mockito 官网 注解

  2. kettle安装文件下载(含多版本)

    kettle是一款基于java开发的洗数工具,可以通过图像化的操作界面,拖拉拽的操作方式,实现数据导入导出清洗等功能,还支持编写脚本进行数据处理,功能十分强大. 本文主要记录一下kettle各版本下载 ...

  3. NPOI,给指定的excle创建个下拉框验证

    NPOI,给指定的excle创建个下拉框验证 先大致看下效果吧 Nuget  搜索 NPOI,一般出来的第一个就是,安装NPOI基础环境 1 using NPOI.HSSF.UserModel; 2 ...

  4. 测试获取 Github 信息

    import json import requests from .fetch_github_info import AUTHENTICATED_USER_ENDPOINT, fetch_github ...

  5. 【译】Visual Studio Hub 介绍

    跟上最新的 Visual Studio 更新.特性和资源就像是一项全职工作.我们已经听过一次又一次了--您想要一种更简单的方式来获取信息,而不是在分散的博客文章.发布说明和社交媒体更新中挖掘. 这就是 ...

  6. 动态DP(DDP)

    动态DP是树上的.带修改的DP.修改操作一般而言用树剖加线段树加广义矩阵乘法来维护,复杂度可以达到 \(n\log^2 n\). 叫DDP是不知从哪里延续下来的一种神秘简称. P4719 [模板]动态 ...

  7. Django内置filter总结

    内置过滤器 目的是对绝大多数的内置过滤器进行测试总结学习,现设置如下: urls.py中设置: urlpatterns=[ url(r'^method',views.method,name='meth ...

  8. ISCC2025破阵夺旗赛三阶段Misc详解 By Alexander

    ISCC2025破阵夺旗赛三阶段Misc详解 By Alexander 写在前面:十八天吃石终于结束了,第一次就让我见到了这个比赛有多么的构式,平台是构式的,睡一觉就1000解了,全是对flag的渴望 ...

  9. L2-2、示范教学与角色扮演:激发模型"模仿力"与"人格"

    一.Few-shot 教学的核心原理与优势 在与大语言模型交互时,Few-shot(少样本)教学是一种强大的提示技术.其核心原理是通过提供少量示例,引导模型理解我们期望的输出格式和内容风格. Few- ...

  10. 转-Linux mpstat命令入门-CPU实时监控详解

    简介   mpstat 来自Multiprocessor Statistics的英文缩写,是实时系统监控工具,主要用来查看多CPU系统中每个CPU的负载是否均衡,相关统计信息存放在/proc/stat ...