12

我在一些 OSS 单元测试中经常看到这段代码,但它是线程安全的吗?while 循环是否保证看到 invoc 的正确值?

如果不; nerd 指出谁也知道这可能会在哪个 CPU 架构上失败。

  private int invoc = 0;

  private synchronized void increment() {
    invoc++;
  }

  public void isItThreadSafe() throws InterruptedException {
      for (int i = 0; i < TOTAL_THREADS; i++) {
        new Thread(new Runnable() {
          public void run() {
             // do some stuff
            increment();
          }
        }).start();
      }
      while (invoc != TOTAL_THREADS) {
        Thread.sleep(250);
      }
  }
4

5 回答 5

17

不,它不是线程安全的。invoc 需要声明为 volatile,或者在同一个锁上同步时访问,或者更改为使用 AtomicInteger。仅仅使用同步方法来增加调用,而不是同步读取它,是不够的。

JVM 做了很多优化,包括特定于 CPU 的缓存和指令重新排序。它使用 volatile 关键字和锁定来决定何时可以自由优化以及何时必须具有可供其他线程读取的最新值。因此,当读者不使用锁时,JVM 无法知道不给它一个陈旧的值。

Java Concurrency in Practice (第 3.1.3 节)的这句话讨论了写入和读取如何需要同步:

内在锁定可用于保证一个线程以可预测的方式看到另一个线程的影响,如图 3.1 所示。当线程 A 执行一个同步块,随后线程 B 进入一个由同一个锁保护的同步块时,保证在释放锁之前对 A 可见的变量的值在获取锁时对 B 可见。换句话说,当 A 执行由同一锁保护的同步块时,A 在同步块中或之前所做的一切对 B 都是可见的。没有同步,就没有这样的保证。

下一节(3.1.4)介绍如何使用 volatile:

Java 语言还提供了另一种较弱的同步形式,即 volatile 变量,以确保对变量的更新可预测地传播到其他线程。当一个字段被声明为 volatile 时,编译器和运行时会注意到这个变量是共享的,并且对它的操作不应该与其他内存操作重新排序。易失性变量不会缓存在寄存器或缓存中,它们对其他处理器是隐藏的,因此对易失性变量的读取总是返回任何线程最近的写入。

回到我们的桌面上都有单 CPU 机器的时候,我们会编写代码并且在它运行在多处理器机器上之前永远不会出现问题,通常是在生产中。导致可见性问题的一些因素,例如 CPU 本地缓存和指令重新排序,是任何多处理器机器所期望的。不过,任何机器都可能消除明显不需要的指令。没有什么可以强迫 JVM 让读者看到变量的最新值,你要听从 JVM 实现者的摆布。所以在我看来,这段代码对于任何 CPU 架构都不是一个好的选择。

于 2011-08-04T20:49:24.100 回答
2

出色地!

  private volatile int invoc = 0;

会成功的。

并查看java原始整数是设计的还是偶然的?哪些站点一些相关的java定义。显然 int 很好,但 double & long 可能不是。


编辑,附加。问题是:“看到 invoc 的正确值了吗?”。什么是“正确的价值”?与时空连续体一样,线程之间并不真正存在同时性。上面的一篇文章指出,该值最终会被刷新,另一个线程会得到它。代码是“线程安全的”吗?我会说“是”,因为在这种情况下,它不会基于排序的变幻莫测而“行为不端”。

于 2011-08-04T20:55:05.127 回答
1

从理论上讲,读取可能会被缓存。Java 内存模型中没有任何东西可以阻止这一点。

实际上,这极不可能发生(在您的特定示例中)。问题是,JVM 是否可以跨方法调用进行优化。

read #1
method();
read #2

为了让 JVM 推断 read#2 可以重用 read#1 的结果(可以存储在 CPU 寄存器中),它必须确定不method()包含同步操作。这通常是不可能的——除非method()是内联的,并且 JVM 可以从扁平代码中看到 read#1 和 read#2 之间没有同步/易失性或其他同步操作;然后它可以安全地消除 read#2。

现在在您的示例中,方法是Thread.sleep(). 实现它的一种方法是忙循环某些时间,具体取决于 CPU 频率。然后JVM可能内联它,然后消除read#2。

但是当然这样的实现sleep()是不现实的。它通常被实现为调用 OS 内核的本机方法。问题是,JVM 是否可以跨这种本机方法进行优化。

即使 JVM 了解某些本机方法的内部工作原理,因此可以在它们之间进行优化,也不太可能以sleep()这种方式处理。sleep(1ms)需要数百万个 CPU 周期才能返回,围绕它进行优化以节省一些读取确实没有意义。

--

这个讨论揭示了数据竞争的最大问题——推理它需要付出太多努力。一个程序不一定是错的,如果它没有“正确同步”,但是证明它没有错并不是一件容易的事。如果程序正确同步并且不包含数据竞争,那么生活会简单得多。

于 2011-08-05T11:44:56.253 回答
0

据我了解代码应该是安全的。字节码可以重新排序,是的。但最终 invoc 应该再次与主线程同步。Synchronize 保证 invoc 正确递增,因此在某些寄存器中有一致的 invoc 表示。在某个时候,这个值将被刷新并且小测试成功。

这当然不好,我会选择我投票的答案,会修复这样的代码,因为它有异味。但仔细想想,我会认为它是安全的。

于 2011-08-04T21:10:14.410 回答
0

如果您不需要使用“int”,我建议将 AtomicInteger 作为线程安全的替代方案。

于 2011-08-05T15:06:12.813 回答