找回密码
 立即注册
首页 业界区 业界 【乐观锁实现】StampedLock 的乐观读机制

【乐观锁实现】StampedLock 的乐观读机制

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


  • 写线程饥饿:

    • 在传统的读写锁(ReentrantReadWriteLock)中,读锁是共享的。当有大量读线程持续持有读锁时,写线程可能长时间无法获取写锁(因为写锁需要独占访问),导致写操作被“饿死”。

  • 悲观锁的开销:

    • 即使没有写操作正在进行,传统的读锁在获取和释放时仍然需要一定的同步开销(CAS操作、维护内部状态等)。在超高并发的读场景下,这些开销累积起来可能成为性能瓶颈。

  • 读操作占主导:

    • 在大多数应用场景(如缓存、配置读取)中,读操作的频率远高于写操作。传统读写锁的设计(读优先或公平策略)在这种场景下效率不高。

乐观读机制的核心思想


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

使用 StampedLock 的乐观读遵循以下模式:
  1. import java.util.concurrent.locks.StampedLock;
  2. public class OptimisticReadingExample {
  3.     private final StampedLock lock = new StampedLock();
  4.     private double x, y; // 共享数据
  5.     // 计算距离的方法 (使用乐观读)
  6.     public double distanceFromOrigin() {
  7.         // 1. 尝试乐观读:获取一个戳记(stamp)
  8.         long stamp = lock.tryOptimisticRead();
  9.         // 2. 将共享变量读入本地局部变量(此时数据可能被其他线程修改!)
  10.         double currentX = x;
  11.         double currentY = y;
  12.         // 3. 关键:验证戳记是否仍然有效(自获取戳记以来是否有过写操作?)
  13.         if (!lock.validate(stamp)) {
  14.             // 3a. 验证失败:乐观读期间发生了写操作!戳记已失效。
  15.             //     降级为获取悲观的读锁(会阻塞写,确保读取一致性)
  16.             stamp = lock.readLock(); // 这是一个阻塞调用
  17.             try {
  18.                 // 在悲观读锁保护下重新读取数据
  19.                 currentX = x;
  20.                 currentY = y;
  21.             } finally {
  22.                 // 无论如何都要释放读锁
  23.                 lock.unlockRead(stamp);
  24.             }
  25.         }
  26.         // 3b. 如果验证成功,或者降级后重新读取成功,则使用 currentX 和 currentY 进行计算
  27.         return Math.sqrt(currentX * currentX + currentY * currentY);
  28.     }
  29.     // 写操作方法
  30.     public void move(double deltaX, double deltaY) {
  31.         long stamp = lock.writeLock(); // 获取独占写锁
  32.         try {
  33.             x += deltaX;
  34.             y += deltaY;
  35.         } finally {
  36.             lock.unlockWrite(stamp); // 释放写锁
  37.         }
  38.     }
  39. }
复制代码
关键步骤详解


  • long stamp = lock.tryOptimisticRead();

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

  • 读取共享数据到局部变量 (currentX = x; currentY = y;)

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

  • if (!lock.validate(stamp)) { ... }

    • 这是最核心的步骤。调用 validate(stamp) 检查自你获取乐观读戳记 (stamp) 以来,是否有任何线程成功获取了写锁并修改了数据。
    • 如果返回 true (验证成功):

      • 意味着在你获取 stamp 之后,直到 validate 调用执行的那一刻,没有发生过写操作。你可以确信第 2 步读取到的 currentX 和 currentY 是一致有效的(至少是某个一致状态下的快照)。你可以安全地使用它们进行计算 (return Math.sqrt(...))。

    • 如果返回 false (验证失败):

      • 意味着在你读取数据(第 2 步)的过程中或之后,在 validate 调用之前,至少发生了一次成功的写操作。你第 2 步读取的数据 currentX 和 currentY 可能无效(不一致或过时),绝对不能使用它们!
      • 此时,你需要降级 (downgrade) 为传统的、悲观的策略:获取一个读锁 (stamp = lock.readLock();)。这个调用可能会阻塞,等待当前可能存在的写锁释放。
      • 在获取到读锁后,重新读取共享数据 (x 和 y) 到局部变量 (currentX, currentY)。因为现在持有读锁,所以能保证在读取过程中不会有写操作发生,读取到的数据是一致的。
      • 在 finally 块中释放读锁 (lock.unlockRead(stamp);)。
      • 最后,使用在悲观读锁保护下读取到的、一致的数据进行计算。


  • 使用数据 (return Math.sqrt(...))

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

使用乐观读的注意事项


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

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

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册