id = 1 - id 是原子的吗?

2022-08-31 13:42:05

摘自 OCP Java SE 6 Programmer Practice Exams 第 291 页,问题 25:

public class Stone implements Runnable {
    static int id = 1;

    public void run() {
        id = 1 - id;
        if (id == 0) 
            pick(); 
        else 
            release();
    }

    private static synchronized void pick() {
        System.out.print("P ");
        System.out.print("Q ");
    }

    private synchronized void release() {
        System.out.print("R ");
        System.out.print("S ");
    }

    public static void main(String[] args) {
        Stone st = new Stone();
        new Thread(st).start();
        new Thread(st).start();
    }
}

其中一个答案是:

输出可以是P Q P Q

我将此答案标记为正确。我的理由:

  1. 我们将启动两个线程。
  2. 第一个进入 .run()
  3. 根据 JLS 15.26.1,它首先评估 。结果是 。它存储在线程的堆栈上。我们正要将其保存为静态 ,但是...1 - id00id
  4. Boom,调度程序选择要运行的第二个线程。
  5. 因此,第二个线程进入 。静态是静止的,所以他执行方法。 已打印。run()id1pick()P Q
  6. 调度程序选择要运行的第一个线程。它从其堆栈中获取并保存到静态 。因此,第一个线程也会执行并打印 。0idpick()P Q

然而,在书中写道,这个答案是不正确的:

这是不正确的,因为该行交换了 和 之间的值。同一方法不可能执行两次。id = 1 - idid01

我不同意。我认为我上面介绍的场景有一些机会。这种交换不是原子的。我错了吗?


答案 1

我错了吗?

不,你是绝对正确的 - 就像你的示例时间表一样。

除了它不是原子的之外,还不能保证写入操作无论如何都会被另一个线程拾取,因为没有同步并且字段不是易失性的。id

像这样的参考资料不正确,这有点令人不安:(


答案 2

在我看来,模拟考试中的答案是正确的。在此代码中,您将执行两个有权访问同一静态变量 id 的线程。静态变量存储在java的堆上,而不是堆栈上。runnables的执行顺序是不可预测的。

但是,为了更改每个线程的id值:

  1. 将存储在id的内存地址中的值的本地副本复制到CPU注册表;
  2. 执行操作 。严格来说,这里执行两个操作1 - id(-id and +1);
  3. 将结果移回堆上的内存空间。id

这意味着,尽管 id 值可以由两个线程中的任何一个同时更改,但只有初始值和最终值是可变的。中间值不会相互修改。

此外,对代码的分析可以表明,在任何时间点,id只能是0或1。

证明:

  • 起始值 id = 1;一个线程会将其更改为 0 ( )。另一个线程会将其恢复到1。id = 1 - id

  • 起始值 id = 0;一个线程会将其更改为 1 ( )。另一个线程会将其恢复到 0。id = 1 - id

因此,id 的值状态是离散的 0 或 1。

证明结束。

此代码可能有两种可能性:

  • 可能性 1.线程 1 首先访问变量 id。然后 id ( 的值更改为 0。此后,将仅执行该方法,打印 。线程二,将评估当时的id;然后执行方法打印 R S。结果,将被打印出来。id = 1 - idpick ()P Qid = 0release()P Q R S

  • 可能性 2.线程二首先访问变量 id。然后 id ( 的值更改为 0。此后,将仅执行该方法,打印 。线程一,将在那时评估id;然后执行方法打印 R S。结果,将被打印出来。id = 1 - idpick ()P Qid = 0release()P Q R S

没有其他可能性。但是,应该注意的是,诸如 或 等的变体可能是由于是静态方法而打印的,因此在两个线程之间共享。这会导致同时执行此方法,这可能导致根据您的平台以不同的顺序打印字母。P Q R SP R Q SR P Q Spick()

但是,在任何情况下,都不会执行该方法或执行两次,因为它们是互斥的。因此不会是输出。pick()release ()P Q P Q