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. 最详细最易懂的【YOLOX原理篇】

    目录 前言 简介 详细解读 Mosaic and Mixup Mixup Mosaic Decoupled Head anchor free SimOTA in_boxes 和 in_center 计 ...

  2. mac 系统软件推荐

    幕布: https://mubu.com/home 石墨文档: https://shimo.im

  3. devops组件搭配选型

    名称 作用 备注 sentry 异常捕获系统 gitlab 代码仓库 jenkins 持续集成 open-falcon 监控系统 grafana 监控FE prometheus 监控系统 thanos ...

  4. Web客户端开发

    Web开发工具 从高层次来看,可以将客户端工具放入以下三大类需要解决的问题中: 安全网络 - 在代码开发期间有用的工具. 转换 - 以某种方式转换代码的工具,例如将一种中间语言转换为浏览器可以理解的 ...

  5. 【翻译】 Processing系列|(三)安卓项目构建

    上上篇:[翻译]Processing系列|(一)简介及使用方法 上一篇:[翻译]Processing系列|(二)安卓模式的安装使用及打包发布 我顺藤摸瓜找到了Github仓库,然后发现人家主要还是用A ...

  6. 【经验】CiteSpace|Wiley Online Library或除知网以外的其他网站的文献怎么导入CiteSpace 6.1.6?

      如果没安装,请看这篇博客安装,现在新版(6.1.6)的不需要额外下载java了,就很妙~:   最新版citespace软件的安装与配置   结论:导出成RIS然后用它自带的转换成WoS. 文章目 ...

  7. 🧠 30 个 MCP 项目创意(附完整源码)

    MCP(Model Context Protocol)是一种新兴的开放协议,旨在标准化应用程序如何向大型语言模型(LLMs)提供上下文和工具.它允许 AI 代理与实际工具和应用程序交互,从而实现复杂的 ...

  8. 轮播图导航组件 | 纯血鸿蒙组件库AUI

    摘要: 轮播图导航(A_SwiperNav):实现沉浸式体验的App全屏轮播引导页效果.可设置图片数据(含文本.图片地址.路由.标题.子标题),可设置按钮颜色. 一.在页面当中调用轮播图导航组件 打开 ...

  9. Gin RBAC 权限基础实现

    RBAC (基于角色的访问控制) 是一种广泛应用的权限管理模型, 通过 角色 将 用户 和 权限 解耦, 简化权限分配管理. 用户 (User): 系统的使用者 权限 (Permission): 对资 ...

  10. RPC实战与核心原理之网络通信

    架构设计:涉及一个灵活的RPC框架 回顾 RPC的通信原理及RPC中各个功能组件的作用 RPC就是把拦截到的方法参数,转成可以在网络中传输的二进制,并保证服务提供方能正确还原出语义,最终实现想调用本地 ...