【问题标题】:compareAndSet memory effects of unsuccessful operationscompareAndSet 不成功操作的记忆效应
【发布时间】:2015-04-24 11:28:48
【问题描述】:

Java 通过其原子类公开 CAS 操作,例如

boolean compareAndSet(expected,update)

JavaDocs 指定 compareAndSet 操作的记忆效果如下:

compareAndSet 和所有其他读取和更新操作 如 getAndIncrement 具有两者的记忆效应 读取和写入 volatile 变量。

这绝对适用于成功的compareAndSet 调用。但是如果 compareAndSet 返回false,记忆效应是否也成立?

我会说不成功的compareAndSet 对应于易失性读取(因为在这种情况下必须访问原子实例的当前值),但我不明白为什么 CAS 应该执行特殊的内存屏障指令失败的案例。

问题实际上是,不成功的 CAS 是否也建立了先发生关系。考虑以下程序:

public class Atomics {
    private static AtomicInteger ai = new AtomicInteger(5);
    private static int x = 0;

    public static void main(String[] args) {
        new Thread(() -> {
            while (x == 0) {
                ai.compareAndSet(0, 0); // returns false
            }
        }, "T1").start();

        new Thread(() -> {
            x = 1;
            ai.compareAndSet(0, 0); // returns false
        }, "T2").start();
    }
}

线程 T2(和程序)一定会终止吗?

【问题讨论】:

  • 他们为什么不呢?
  • 好吧,如果规范没有要求,那么 VM 可以省略执行其他必要的屏障指令。
  • @Dominik 我不认为你的程序一定会终止。

标签: java concurrency java-memory-model compare-and-swap


【解决方案1】:

使用volatile 读取和写入建立happens-before 关系的问题在于,这种关系仅存在于写入和后续 读取。如果一个线程 T1 写入共享的 volatile 变量,而另一个线程 T2 从同一个变量中读取,则如果 T2 在 T1 写入变量之前读取该变量,则不会存在 happens-before 关系。如果决定 T1 是否在 T2 读之前写是线程调度,我们没有任何保证。

在没有额外同步的情况下处理它的一种实用方法是评估 T2 读取的实际值。如果这个值表明 T1 已经写入了新值,那么我们就有一个有效的 happens-before 关系。这就是使用volatile boolean fooIsInitialized 标志或volatile int currentPhase 计数器时的工作方式。很明显,如果写入的值与旧值相同,或者从未实际写入新值,则此方法不起作用。

您的示例程序的问题在于它推测了线程调度。它假定 T2 最终执行 cas 动作,并且在 T1 中将有一个 后续 迭代,其中下一个 cas 将创建 happens-before 关系。但这不能保证。它可能无法直观地理解,但如果没有同步,T1 的所有迭代都可能发生在 T2 的动作之前,即使循环是无限的。这甚至是一种有效的线程调度行为,让 T1 永远运行消耗 100% 的 CPU 时间,然后再将 CPU 时间分配给 T2,因为不能保证在 同等优先级 的线程之间进行抢占式线程切换。

但是,即使底层系统确实为 T2 分配了 CPU 时间,最终将执行这些操作,JVM 也没有必要让 T1 明白这一点,因为 T1 无法观察到 T2 曾经运行过。在现实生活中不太可能发现这一点,但答案仍然是没有保证。当有一连串动作可以让 T1 观察到 T2 运行(即更改其状态)时,情况会发生变化,但当然,该动作链会使 cas 过时。

【讨论】:

    【解决方案2】:

    我喜欢 Holger 的回答,并且由于技术信息会接受这一点,但我会写一个结果相同但观点不同的答案。这个程序有可能永远运行吗?是的,考虑可能的编译器重新排序。

        new Thread(() -> {
            if(x == 0){
                while (true) {
                    ai.compareAndSet(0, 0); // returns false
                }
            }
        }, "T1").start();
    

    这是可能的重新订购吗?是的。没有规则说尽管随后发生了易失性存储,但此处不会发生提升。这里唯一的规则是读取不能在下面易失性存储区执行。

    编辑:我意识到问题是关于记忆效应,而不是编译器排序。我会留下这个答案,因为它可能有用但并不能真正回答问题。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2020-08-22
      • 2010-12-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-02-24
      • 1970-01-01
      相关资源
      最近更新 更多