【问题标题】:Is id = 1 - id atomic?id = 1 - id 是原子的吗?
【发布时间】:2015-01-21 05:48:27
【问题描述】:

来自 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 - id。结果是0。它存储在线程的堆栈中。我们正准备将 0 保存为静态 id,但是...
  4. Boom,调度程序选择了第二个线程来运行。
  5. 所以,第二个线程进入run()。静态的id还是1,所以他执行方法pick()P Q 已打印。
  6. 调度程序选择第一个线程来运行。它从堆栈中取出0 并保存到静态id。因此,第一个线程也执行pick() 并打印P Q

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

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

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

【问题讨论】:

  • 顺便说一句,他们允许 R S R S 吗?
  • @JonSkeet 没有这样的答案。他们允许P Q R SP R S QP R Q S,我同意。
  • 我认为您将您所指的 JLS 部分断章取意。该部分介绍了简单的分配(如在单个线程中)。我认为您需要查看JLS 17.4. Memory Model
  • 当然P R S QP R Q S 也不可能,因为pickrelease 是同步的。我错过了什么吗(我的 Java 可能有点生疏了)?
  • 在原始代码示例(来自提到的书)中,release 方法不是静态的。所以P R S QP R Q S 确实可能的解决方案。然而,这并不能解决run 方法中的竞争条件,因此本书在这个问题上仍然是错误的。

标签: java multithreading atomic swap scjp


【解决方案1】:

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

但是,为了改变每个线程的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 id = 0;然后方法 release() 将是执行打印 R S。结果,P Q R S 将被打印出来。

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

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

但是无论如何,方法pick()release () 永远不会被执行两次,因为它们是互斥的。因此P Q P Q 不会是输出。

【讨论】:

  • 在您的第 2 步中,1 - d(零)的结果将存储在线程的堆栈中。现在,在第 3 步之前,调度程序可以更改正在运行的线程。因此,第二个线程也将从1 - d 获得零。请仔细研究我在问题中提出的场景。这就是为什么P Q P Q 是可能的。如果您仍然不相信本页上已经提供的理论分析,请同时查看最后一篇帖子 here,那里的经验表明这是可能的。
  • 您的“证明”省略了两种情况 - 起始值为 1,两个 线程都可以尝试将其更改为 0;并且起始值为 0,两个 线程都可以尝试将其更改为 1。此答案不正确。
【解决方案2】:

我错了吗?

不,您是绝对正确的 - 您的示例时间线也是如此。

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

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

【讨论】:

  • 谢谢,但您的意思是不能保证写到id 的内容无论如何都会被其他线程接收?它可以以某种方式优化并且第二个线程不能写入id?我不明白那部分。
  • @AdamStelmaszczyk:不,我的意思是线程 1 可以将新值写入 id,但线程 2 可能不会立即看到它 - 它可能会看到 old 值.
  • 在你的 CPU 引擎盖下潜藏着巨龙。 Jon 谈到的特定问题称为“缓存一致性”。当你询问一个变量的值时,每次都去主存会太慢(比你想要的慢数百倍!)。为了解决这个问题,现代处理器都有一个他们首先查看的特定于 CPU 或特定于内核的内存缓存。这意味着一个线程可以更改内存中id 的“官方”值,而另一个线程永远不会看到它,因为它永远不会比自己的缓存更远。
  • 有一整套工具可以处理缓存一致性,从显式缓存刷新命令到原子操作再到同步。幸运的是,对于那些从来不想与这些龙打交道的人来说,与synchronized 或互斥锁同步通常会“做你认为应该做的事情”,只要你使用它们使用标准模式来保护你的数据(比如synchronized 在上面的例子中)。很遗憾他们把id弄错了。
  • 值得注意的是,作者甚至admitted there might be something wrong with the question(因为有),尽管我猜他们从来没有费心提供勘误表。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-10-23
  • 1970-01-01
  • 2019-04-27
  • 1970-01-01
  • 1970-01-01
  • 2011-02-15
  • 2012-04-04
相关资源
最近更新 更多