我最近偶然发现了jcstress中的this个例子:

@JCStressTest
@State
@Outcome(id = "10",                      expect = ACCEPTABLE,             desc = "Boring")
@Outcome(id = {"0", "1"},                expect = FORBIDDEN,              desc = "Boring")
@Outcome(id = {"9", "8", "7", "6", "5"}, expect = ACCEPTABLE,             desc = "Okay")
@Outcome(                                expect = ACCEPTABLE_INTERESTING, desc = "Whoa")
public static class Volatiles {
    volatile int x;

    @Actor
    void actor1() {
        for (int i = 0; i < 5; i++) {
            x++;
        }
    }

    @Actor
    void actor2() {
        for (int i = 0; i < 5; i++) {
            x++;
        }
    }

    @Arbiter
    public void arbiter(I_Result r) {
        r.r1 = x;
    }
}

作者强调,由于增量不是原子操作,所以不能指望每次循环迭代只会"丢失"一个增量更新.因此,除了01之外,所有的结果(最多10个)都是允许的(并且确实发生了).

我明白为什么不允许0了:正如JLS中所述,在对象的缺省值的初始化和每个线程中的第一个操作之间存在HB边缘. JLS 17.4.4

将缺省值(零、假或空)写入每个变量会与每个线程中的第一个操作同步.

作者还解释了如何获得结果2:

The most interesting result, "2" can be explained by this interleaving:
        Thread 1: (0 ------ stalled -------> 1)     (1->2)(2->3)(3->4)(4->5)
        Thread 2:   (0->1)(1->2)(2->3)(3->4)    (1 -------- stalled ---------> 2)

我理解你不能像这样"延伸"上面的解释:

        Thread 1: (0 --------- stalled -----------> 1)     (1->2)(2->3)(3->4)(4->5)
        Thread 2:   (0->1)(1->2)(2->3)(3->4)(4->5)

因为它会导致结果5. 但是,难道不存在一个可以产生1的死刑吗? 我绝对想不出有什么.但为什么真的没有呢?

推荐答案

首先,让我们记住volatile在Java中是如何工作的.

In Java all volatile reads and writes happen in a global order in runtime (i.e. synchronization order in JLS 17.4.4).
Properties of this global order:

  • 它保留每个线程的易失性读/写顺序(就JLS 17.4.4it is consistent with the program order而言)
  • 每次执行一个:在同一程序的不同执行中,来自不同线程的操作可以不同地交织

易失性读取始终返回对此变量的最后一次易失性写入(按此全局顺序)(即,JLS 17.4.4中的synchronizes-with).

其次,让我们澄清一下,"增量不是原子操作"意味着x++由3个原子操作组成:

var temp = x;     // volatile read of 'x' to local variable 'temp'
temp = temp + 1;  // increment of local variable 'temp'
x = temp;         // volatile write to `x`

最后,让我们重写

The most interesting result, "2" can be explained by this interleaving:
        Thread 1: (0 ------ stalled -------> 1)     (1->2)(2->3)(3->4)(4->5)
        Thread 2:   (0->1)(1->2)(2->3)(3->4)    (1 -------- stalled ---------> 2)

以一种显示对x的易失性读写的方式:

Thread1    Thread2
r₁₀:0                  | global
            r₂₀:0      | order of
            w₂₁:1      | volatile
            r₂₂:1      | reads
            w₂₃:2      | and
            r₂₄:2      | writes
            w₂₅:3      V
            r₂₆:3
            w₂₇:4
w₁₁:1
            r₂₈:1
r₁₂:1
w₁₃:2
r₁₄:2
w₁₅:3
r₁₆:3
w₁₇:4
r₁₈:4
w₁₉:5
            w₂₉:2

在此图中:

  • 易失性读取和写入的全局顺序是从上到下
  • r:1是对x的易失性读取,它返回1
  • w:11x的易失性写入

请注意:

  • w始终将the same thread中前一个r的值加1
  • r始终返回the global order中前一个w写入的值

现在我们可以回答你的问题了:

但是,不是有一个能产生1个的死刑吗?我绝对想不出有什么.但为什么真的没有呢?

  1. look at the last write in the global order w₂₉: it always writes (the value read by r₂₈)+1
    (BTW the last write in the global order will always be either the last write w₁₉ in Thread1 or the last write w₂₉ in Thread2; in this case it's w₂₉, but for w₁₉ the reasoning is the same)

  2. r₂₈始终以全局顺序读取上一次写入,即:

    • 或者来自同一线程的w₂₇
    • 或者从线程1写入w₁ₓ(如果w₁ₓ在运行时发生在w₂₇r₂₈之间)

    In any case r₂₈ always returns some previous non-initial write.
    But every non-initial write always writes at least 1.
    That means w₂₉ always writes at least 2.

Java相关问答推荐

获取拦截器内部的IP地址

当耗时的代码完成时,Circular ProgressIndicator显示得太晚

表格栏上的事件过滤器在PFA中不起作用

Java模式匹配记录

neo4j java驱动程序是否会在错误发生时自动回滚事务?

CAMEL 4中的SAXParseException

@org.springframework.beans.factory.annotation.Autowired(required=true)-注入点有以下注释:-SpringBoot

Java Swing:初始化身份验证类后未检测到ATM_Interface键事件

这是什么Java构造`(InputStream Is)->;()->;{}`

为什么使用JDK21获取锁定锁比使用JDK11慢

按属性值从流中筛选出重复项

测试何时使用Mockito强制转换对象会导致ClassCastException

GetChildren().emoveAll()不会删除 node

Spring Boot&;Docker:无法执行目标org.springframework.boot:spring-boot-maven-plugin:3.2.0:build-image

在settings.gradle.kts和Build.gradle.kts中使用公共变量

基于配置switch 的@Controller的条件摄取

如何在透视表中添加对计数列的筛选?

可以';不要在Intellij IDEA中使用最新的Java版本(JDK 21)

Swagger.io OpenApi v3.0 声明默认媒体类型

在java中使用SevenZip.openArchive方法后无法删除文件