为什么在StampedLock中不需要使用volatile关键字?

11

给定来自Oracle文档的代码示例https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/locks/StampedLock.html

class Point {
   private double x, y;
   private final StampedLock sl = new StampedLock();

   void move(double deltaX, double deltaY) { // an exclusively locked method
     long stamp = sl.writeLock();
     try {
       x += deltaX;
       y += deltaY;
     } finally {
       sl.unlockWrite(stamp);
     }
   }

   double distanceFromOrigin() { // A read-only method
     long stamp = sl.tryOptimisticRead();
     double currentX = x, currentY = y;
     if (!sl.validate(stamp)) {
        stamp = sl.readLock();
        try {
          currentX = x;
          currentY = y;
        } finally {
           sl.unlockRead(stamp);
        }
     }
     return Math.sqrt(currentX * currentX + currentY * currentY);
   }

   void moveIfAtOrigin(double newX, double newY) { // upgrade
     // Could instead start with optimistic, not read mode
     long stamp = sl.readLock();
     try {
       while (x == 0.0 && y == 0.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);
     }
   }
 }

假设所有 Point 类的方法都可以从不同的线程调用:

为什么我们不需要将 x 和 y 字段声明为 volatile?

执行 Point#moveIfAtOrigin 方法的代码是否保证在获取 StampedLock#readLock 后始终看到 x 和 y 字段的最新更改?

调用 StampedLock#writeLockStampedLock#readLock 时是否建立了任何类型的内存屏障?

有没有人能引用相关文档中的语句?


7
一般而言,获取和释放锁会创建一个内存屏障(memory barrier),因此在锁内(无论是StampedLock还是其他锁)访问变量将确保可见性。 - Kayaman
5
请转到链接https://docs.oracle.com/javase/8/docs/api/index.html?java/util/concurrent/package-summary.html并向下滚动至“内存一致性属性”以获取保证和重要部分 - “ java.util.concurrent及其子包中所有类的方法都扩展这些保证以实现更高级别的同步。 特别是:[...]” - pvg
@pvg 感谢引用文档,这应该足以作为正式证明。 - Dmitry Gorbunov
5
@DmitryGorbunov 这个问题有一点元回答,因为在SO上有许多变体的这个问题,它们略有不同,涉及不同类别的java.util.concurrent - 全套保证和对JMM/JLS的引用很长,并且它们没有在每个类的文档中重复,更不用说每个方法了,但它们在包(和子包)文档中有概述。所以如果你遇到像这样的情况,那么这是首先要检查的地方(不是完全显而易见的)。 - pvg
2个回答

2
我不知道为什么文档中没有明确说明 - 可能是因为它被隐含了,但在内部,它执行了一个Unsafe.compareAndSwapLong,这将转换为LOCK CMPXCHG,在x86上具有full memory barrier(我假设其他平台也会执行类似操作); 因此,确实没有必要将它们设置为volatile
实际上,在x86上的任何带有lock的指令都将具有完整的内存屏障。

2
它在文档中明确引用。 - pvg
@Eugene 这应该足以解释了。我更新了你的答案,包括了pvg引用页面的引用。感谢你的帮助! - Dmitry Gorbunov

2
Lock接口的Javadoc中声明如下:

内存同步

所有锁实现必须强制执行与内置监视器锁提供的内存同步语义相同,如Java语言规范(17.4内存模型)中所述:

成功的锁定操作具有与成功的锁定操作相同的内存同步效果。 成功的解锁操作具有与成功的解锁操作相同的内存同步效果。 不成功的锁定和解锁操作以及可重入的锁定/解锁操作不需要任何内存同步效果。

尽管StampedLock没有实现Lock,但它有一个像asReadLock()这样的方法:

返回此StampedLock的普通Lock视图,其中Lock.lock()方法映射到readLock(),其他方法类似。

它返回StampedLock的内部类ReadLockView的实例,它是Lock的实际实现。
但由于它只是一个委托者,这意味着原始方法必须创建内存屏障,以遵守Lock接口的内存同步强制执行。

这并不是一个令人满意的答案,但它是一个正确且易于理解的结论。 我不确定为什么有人给你的帖子点了踩。它绝对增加了价值。我给它点了赞。 - Dmitry Gorbunov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接