id = 1-id是原子吗?


74

在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为static id,但是...
  4. 繁荣,调度程序选择第二个线程来运行。
  5. 因此,第二个线程进入run()。静态id仍然是1,所以他执行方法pick()P Q打印。
  6. 调度程序选择要运行的第一个线程。它0从堆栈中取出并保存为static id。因此,第一个线程也将执行pick()并打印P Q

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

这不正确,因为线路id = 1 - id交换价值id之间01。相同的方法不可能两次执行。

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


他们是否允许RSRS?
乔恩·斯基特

1
@JonSkeet没有这样的答案。他们允许P Q R SP R S QP R Q S,对此我同意。
2014年

我认为您脱离上下文引用了JLS部分。该部分介绍了简单的分配(如在单个线程中)。我认为您需要查看JLS 17.4。记忆模型
hfontanez 2014年

1
当然P R S QP R Q S也是不可能的,因为pickrelease是同步的。我是否缺少某些东西(我的Java可能有点生锈)?
哑光

2
在原始代码示例(来自所述书籍)中,该release方法不是静态的。因此P R S Q,确实P R Q S 可能的解决方案。然而,这并不能解决方法中的竞争条件run,因此本书在这个问题上还是错误的。
isnot2bad 2014年

Answers:


78

我错了吗?

不,您是完全正确的-您的示例时间表也是如此。

除了没有原子性之外id,考虑到没有同步并且字段不是可变的,因此也不能保证该写操作仍将被其他线程拾取。

像这样的参考资料有些不正确:(


1
谢谢,但是您是什么意思,id意味着不能保证该写操作也将被另一个线程接收?可以以某种方式对其进行优化,并且id第二个线程无法写入?我不明白那部分。
亚当·斯特尔马什奇克

9
@AdamStelmaszczyk:不,我的意思是线程1可以向写入新值id,但是线程2可能不会立即看到它-可能会看到值。
乔恩·斯基特

25
在您的CPU的深层隐藏着巨龙。Jon谈论的特定问题称为“缓存一致性”。当您要求一个变量的值时,每次进入主存储器的速度将太慢(比您想要的速度慢数百倍!)。为了解决这个问题,现代处理器都首先具有特定于CPU或特定于内核的内存缓存。这意味着一个线程可以更改id内存中的“官方”值,而另一个线程则永远看不到它,因为它看起来比它自己的缓存更远。
Cort Ammon 2014年

6
从显式缓存刷新命令到原子再到同步,有一系列用于处理缓存一致性的工具。幸运的是那些谁永远永远永远永远要对付那些龙,同步synchronized或互斥一般不会“你认为它应该做的,”只要你使用它们使用标准模式来保护您的数据(如synchronized在本例以上)。他们弄错了真是可惜id
Cort Ammon 2014年

9
值得一提的是,作者甚至承认这个问题可能存在问题(因为存在),尽管我猜他们从来没有费心提出勘误表。
蒂姆·斯通·史东

-3

我认为,实践考试中的答案是正确的。在此代码中,您正在执行两个可以访问相同静态变量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。线程2将在那时评估id id = 0release()然后将执行方法,打印RS。结果P Q R S将被打印。

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

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

但是无论如何,由于方法pick()或方法release ()互斥的,因此永远不会执行两次。因此P Q P Q将不会是输出。


2
在第2步中,1 - d(零)的结果将存储在线程的堆栈中。现在,在第3步之前,调度程序可以更改正在运行的线程。因此,第二个线程的from也将为零1 - d。请仔细研究我在一个问题中提出的方案。这就是为什么P Q P Q是可能的。如果您仍然对本页上已经提供的理论分析不满意,也请查看此处的最后一篇文章,该文章根据经验表明是可能的。
亚当·斯特尔马什奇克

您的“证明”省略了两种情况-起始值为1,两个线程都可以尝试将其更改为0;并且起始值为0,两个线程都可以尝试将其更改为1。此答案不正确。
戴伍德·本·卡里姆
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.