【问题标题】:Can Atomic values change during an "&&" operation?原子值可以在“&&”操作期间更改吗?
【发布时间】:2022-01-20 14:37:46
【问题描述】:

我知道下一个场景:(奇怪的格式,我知道)

private final AtomicBoolean aBoolean = new AtomicBoolean(true);

public void doSomething() {
    if (
        aBoolean.get()                       // line A
            &&                               // line B
        aBoolean.compareAndSet(true, false)  // line C
        ) {
        System.out.println("Was true!")
    }
}

如果线程#1 和线程#2 同时输入doSomething(),就会发生这种情况:

  1. 线程#1 和线程#2 将aBoolean.get() 读取为== “真”同时。

  2. 两者都将执行“&&”运算符。

  3. CMPXCHG 指令同时为两个线程启动:

    3.1 本地使用LOCK前缀

    3.2 线程 #1 或 #2 先到达,赢得比赛。

    3.3 获胜线程比较(aBoolean == true 吗?)这将返回“true”,因此 aBoolean 将设置为“false”。

    3.4 aBoolean 现在为假。

    3.5 丢失的线程比较(是 aBoolean == true 吗?)这将返回“false”,从而使任何进一步的操作短路。

  4. 获胜线程将打印“是真的!”。

从“失败”线程的角度来看,“A 行”中的第一个 aBoolean.get() 是……假设……一个“谎言”。

现在假设执行可以发生在运算符之间,就像上面示例中所做的那样,让我们​​为第二种情况添加第二种方法:

public void unluckySet() {
    aBoolean.set(false);
}

假设线程 #3 恰好在我们的“获胜线程”到达执行“&&”的“B 行”的确切时刻到达并执行unluckySet()

如果获胜线程到达“B 行”,则意味着它到达“A 行”,aBoolean 为“真”。

我的问题是:

CMPXCHG 是否会将更新后的值正确读取为“假”?这意味着.set() 也被与compareAndSet() 相同的锁持有。

并发和线程之间:

运算符(“&&”、“||”、“==”、“=”,甚至可能是“return;”?这样两者都以交错的方式结束,防止可能的冲突?

【问题讨论】:

  • 是的,他们可以。原子值仅在单个操作中是原子的。 atomicOp1() && atomicOp2() 是 2 个独立的原子操作
  • 请注意,AtomicBoolean 没有什么特别之处,jvm 不会以任何方式特殊对待它们,因此与它们的两个独立交互就是:独立。
  • 谢谢@Felix 我现在正在查看一些代码 [JCTools Java Concurrency Tools for the JVM。 ](github.com/JCTools/JCTools/blob/master/jctools-core/src/main/…),我看到在“CAS_key(”的自旋锁开始的第 666 行和甚至不在自旋锁中的第 775 行“CAS_val(”之间发生了太多事情,我猜那不是完全线程安全...正确吗?该代码已被分叉 444 次和 2.9k 星评级。
  • ...更正,有一个自旋锁,但在形式上是一种自引用方法...这仍然不完全安全,因为即使在运算符之间也可能发生变化...
  • 嗯,像int dummy = DUMMY_VOLATILE; 这样的行表明这段代码被设计为恰好在特定实现下工作,充其量。

标签: java multithreading concurrency atomic interleave


【解决方案1】:

在没有数据竞争(您的程序没有)的情况下,Java 内存模型是顺序一致性。这是非常强大的;它说你程序中的所有读写形成一个总顺序,这与程序顺序一致。因此,您可以想象来自不同线程的读取和写入只是以某种方式交错或混洗在一起 - 而不会改变从同一线程执行的操作的相对顺序。

但是为了这个目的,每个动作都是这个顺序的一个单独的元素。 所以仅仅因为aBoolean.get()aBoolean.compareAndSet()两个动作而不是一个动作,其他线程可以采取任意数量的其他动作放在它们之间。

这些动作是单个语句的一部分还是不同语句的一部分并不重要;或者他们以什么样的表情出现;或者他们之间有什么运营商(如果有的话);或者线程本身可能会或可能不会围绕它们进行哪些计算。没有办法让两个动作“如此接近”,以至于在它们之间不会发生任何其他事情,除非用语言定义为原子的单个动作替换它们。


在机器级别,一个非常简单的方法可以发生这种情况,因为aBoolean.get()aBoolean.compareAndSet() 几乎可以肯定是两个不同的机器指令,中断可能会到达它们之间。这个中断可能导致线程被延迟任意时间,在此期间其他线程可以做任何他们想做的事情。所以线程#1 和#2 完全有可能在它们的get()compareAndSet() 之间都被中断,而线程#3 同时执行它的set

注意:关于特定机器如何工作的推理通常有助于理解为什么可能出现不希望的行为,如上一段所述。但它不能替代对正式内存模型的推理,也不应该用来试图争论一个程序必须有它想要的行为。即使你心目中的一台特定机器会为你的代码做正确的事情,或者你想不出一个看似合理的机器会发生故障的方式,但这并不能证明你的程序是正确的。

所以试图说“哦,机器会执行lock cmpxchg 等等等等等等,一切正常”是不明智的;您没有想到的其他一些机器可能以完全不同的方式工作,它仍然符合抽象 Java 内存模型,但在其他方面违反了您基于 x86 的期望。事实上,x86 是一个特别糟糕的例子:由于历史原因,它提供了一组相当强大的指令级内存排序保证,而许多其他“弱排序”架构则没有,因此可能有很多东西是 Java 抽象的允许,但 x86 在实践中不会这样做。

【讨论】:

  • 请注意,规范中没有提到 compareAndSet 被编译为 lock cmpxchg,因此即使在 x86 下运行,您也不能使用该指令的行为来推断 Java 会的。只要遵守 JMM,JIT 编译器和优化器就可以随意做任何事情。但在 aBoolean.get() && aBoolean.compareAndSet(true, false) 的特定情况下,这并不重要,因为结果是 truefalse,无论 aBoolean 是否更改为 false之前已经false了。
猜你喜欢
  • 2012-03-28
  • 1970-01-01
  • 2012-04-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多