Id = 1-id 是原子的吗?

从 OCP Java SE 6程序员实践考试第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 - id进行评估。结果是 0。它存储在线程的堆栈上。我们正要把 0保存到静态 id,但是..。
  4. 崩,调度程序选择运行第二个线程。
  5. 因此,第二个线程进入 run()。静态 id仍然是 1,所以他执行方法 pick()P Q被打印。
  6. 计划程序选择要运行的第一个线程。它从堆栈中取出 0并保存到静态 id。因此,第一个线程也执行 pick()并打印 P Q

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

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

我不同意。我认为上面提到的情况还是有可能发生的。这种互换不是原子的。我说错了吗?

3185 次浏览

我说错了吗?

不,你是完全正确的-这是你的示例时间线。

除了它不是原子的以外,还不能保证对 id的写操作会被另一个线程接收,因为没有同步,而且字段也不是易失的。

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

在我看来,实践考试的答案是正确的。在这段代码中,您正在执行两个线程,这两个线程可以访问相同的静态变量 id。静态变量存储在 java 的堆上,而不是堆栈上。可运行程序的执行顺序是不可预测的。

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

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

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

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

证据:

  • 起始值 id = 1; 一个线程把它变成0(id = 1 - id) ,另一个线程把它变回1。

  • 起始值 id = 0; 一个线程将其改为1(id = 1 - id) ,另一个线程将其返回到0。

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

证据结束。

这个代码有两种可能性:

  • 可能性1。线程一首先访问变量 id。然后 id (id = 1 - id的值变为0。然后,只执行方法 pick (),打印 P Q。线程二,将在此时计算 id; 方法 release()将随后执行打印 RS。因此,P Q R S将被打印。

  • 可能性2。线程二首先访问变量 id。然后 id (id = 1 - id的值变为0。然后,只执行方法 pick (),打印 P Q。线程一,将计算 id 在此时 id = 0; 方法 release()将然后执行打印 RS。因此,P Q R S将被打印。

没有其他可能了。然而,应该注意的是,由于 pick()是一个静态方法,因此 P Q R S的变体(如 P R Q SR P Q S等)可能会被打印出来,并在两个线程之间共享。这会导致该方法的同时执行,从而可能导致根据您的平台以不同的顺序打印字母。

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