141

我知道诸如复合操作之类i++的不是线程安全的,因为它们涉及多个操作。

但是检查引用本身是线程安全的操作吗?

a != a //is this thread-safe

我尝试对此进行编程并使用多个线程,但没有失败。我想我无法在我的机器上模拟比赛。

编辑:

public class TestThreadSafety {
    private Object a = new Object();

    public static void main(String[] args) {

        final TestThreadSafety instance = new TestThreadSafety();

        Thread testingReferenceThread = new Thread(new Runnable() {

            @Override
            public void run() {
                long countOfIterations = 0L;
                while(true){
                    boolean flag = instance.a != instance.a;
                    if(flag)
                        System.out.println(countOfIterations + ":" + flag);

                    countOfIterations++;
                }
            }
        });

        Thread updatingReferenceThread = new Thread(new Runnable() {

            @Override
            public void run() {
                while(true){
                    instance.a = new Object();
                }
            }
        });

        testingReferenceThread.start();
        updatingReferenceThread.start();
    }

}

这是我用来测试线程安全的程序。

奇怪的行为

当我的程序在一些迭代之间开始时,我得到了输出标志值,这意味着引用!=检查在同一个引用上失败。但是经过一些迭代后,输出变为恒定值false,然后长时间执行程序不会产生单个true输出。

正如输出所暗示的那样,经过一些 n (非固定)迭代后,输出似乎是恒定值并且不会改变。

输出:

对于一些迭代:

1494:true
1495:true
1496:true
19970:true
19972:true
19974:true
//after this there is not a single instance when the condition becomes true
4

8 回答 8

124

在没有同步的情况下这个代码

Object a;

public boolean test() {
    return a != a;
}

可能产生true。这是字节码test()

    ALOAD 0
    GETFIELD test/Test1.a : Ljava/lang/Object;
    ALOAD 0
    GETFIELD test/Test1.a : Ljava/lang/Object;
    IF_ACMPEQ L1
...

正如我们所看到的,它将字段加载a到本地变量两次,这是一个非原子操作,如果a在两者之间被另一个线程比较更改,则可能会产生false.

此外,内存可见性问题在这里是相关的,不能保证a另一个线程所做的更改将对当前线程可见。

于 2013-08-27T08:35:44.133 回答
47

检查a != a线程安全吗?

如果a可能被另一个线程更新(没有适当的同步!),那么不。

我尝试对此进行编程并使用多个线程,但没有失败。我想无法在我的机器上模拟比赛。

那不代表什么!问题是如果JLS允许a执行由另一个线程更新的执行,那么代码不是线程安全的。您不能在特定机器和特定 Java 实现上的特定测试用例中导致竞争条件发生的事实,并不排除它在其他情况下发生。

这是否意味着 a != a 可以返回true

是的,理论上,在某些情况下。

或者,即使在同时发生变化,a != a也可以返回。falsea


关于“奇怪的行为”:

当我的程序在一些迭代之间开始时,我得到了输出标志值,这意味着引用 != 检查在同一个引用上失败。但是经过一些迭代后,输出变为常量值 false,然后长时间执行程序不会生成单个 true 输出。

这种“奇怪”的行为与以下执行场景一致:

  1. 程序被加载,JVM 开始解释字节码。由于(正如我们从 javap 输出中看到的那样)字节码执行了两次加载,因此您(显然)偶尔会看到竞争条件的结果。

  2. 一段时间后,代码由 JIT 编译器编译。JIT 优化器注意到同一内存插槽 ( a) 的两个负载靠近,并优化第二个。(事实上​​,它有可能完全优化测试......)

  3. 现在竞争条件不再出现,因为不再有两个负载。

请注意,这与 JLS 允许 Java 的实现执行的操作完全一致


@kriss 如此评论:

这看起来可能是 C 或 C++ 程序员所说的“未定义行为”(取决于实现)。在像这样的极端情况下,Java 中似乎可能有一些 UB。

Java 内存模型(在JLS 17.4中指定)指定了一组前提条件,在这些前提条件下,一个线程可以保证看到另一个线程写入的内存值。如果一个线程试图读取另一个线程写入的变量,并且不满足这些先决条件,那么可能会出现许多可能的执行……其中一些可能是不正确的(从应用程序需求的角度来看)。换句话说,定义了一可能的行为(即一组“格式良好的执行”),但我们不能说哪些行为会发生。

如果代码的最终效果相同,则允许编译器组合和重新排序加载和保存(以及做其他事情):

  • 当由单个线程执行时,并且
  • 当由正确同步的不同线程执行时(根据内存模型)。

但是,如果代码没有正确同步(因此“发生在之前”的关系不足以限制格式良好的执行集),则允许编译器以会给出“不正确”结果的方式重新排序加载和存储。(但这实际上只是说程序不正确。)

于 2013-08-27T08:32:04.053 回答
27

用 test-ng 证明:

public class MyTest {

  private static Integer count=1;

  @Test(threadPoolSize = 1000, invocationCount=10000)
  public void test(){
    count = new Integer(new Random().nextInt());
    Assert.assertFalse(count != count);
  }

}

我在 10 000 次调用中有 2 次失败。所以,它不是线程安全的

于 2013-08-27T08:48:04.593 回答
15

不它不是。对于比较,Java VM 必须将要比较的两个值放在堆栈上并运行比较指令(其中一个取决于“a”的类型)。

Java VM 可以:

  1. 读“a”两次,把每一个都放在栈上,然后比较结果
  2. 只读取一次“a”,将其放入堆栈,复制它(“dup”指令)并运行比较
  3. 完全消除表达式并将其替换为false

在第一种情况下,另一个线程可以在两次读取之间修改“a”的值。

选择哪种策略取决于 Java 编译器和 Java 运行时(尤其是 JIT 编译器)。它甚至可能在程序运行期间发生变化。

如果您想确定如何访问变量,您必须创建它volatile(所谓的“半内存屏障”)或添加一个完整的内存屏障(synchronized)。您还可以使用一些更高级别的 API(例如AtomicIntegerJuned Ahasan 提到的)。

有关线程安全的详细信息,请阅读JSR 133Java 内存模型)。

于 2013-08-27T08:42:47.357 回答
6

Stephen C 已经很好地解释了这一切。为了好玩,您可以尝试使用以下 JVM 参数运行相同的代码:

-XX:InlineSmallCode=0

这应该会阻止 JIT 进行的优化(它在热点 7 服务器上进行)并且您将true永远看到(我停在 2,000,000 但我想它在那之后会继续)。

有关信息,以下是 JIT 代码。老实说,我没有足够流利地阅读汇编,不知道测试是否真的完成了或者这两个负载来自哪里。(第 26 行是测试flag = a != a,第 31 行是 的右括号while(true))。

  # {method} 'run' '()V' in 'javaapplication27/TestThreadSafety$1'
  0x00000000027dcc80: int3   
  0x00000000027dcc81: data32 data32 nop WORD PTR [rax+rax*1+0x0]
  0x00000000027dcc8c: data32 data32 xchg ax,ax
  0x00000000027dcc90: mov    DWORD PTR [rsp-0x6000],eax
  0x00000000027dcc97: push   rbp
  0x00000000027dcc98: sub    rsp,0x40
  0x00000000027dcc9c: mov    rbx,QWORD PTR [rdx+0x8]
  0x00000000027dcca0: mov    rbp,QWORD PTR [rdx+0x18]
  0x00000000027dcca4: mov    rcx,rdx
  0x00000000027dcca7: movabs r10,0x6e1a7680
  0x00000000027dccb1: call   r10
  0x00000000027dccb4: test   rbp,rbp
  0x00000000027dccb7: je     0x00000000027dccdd
  0x00000000027dccb9: mov    r10d,DWORD PTR [rbp+0x8]
  0x00000000027dccbd: cmp    r10d,0xefc158f4    ;   {oop('javaapplication27/TestThreadSafety$1')}
  0x00000000027dccc4: jne    0x00000000027dccf1
  0x00000000027dccc6: test   rbp,rbp
  0x00000000027dccc9: je     0x00000000027dcce1
  0x00000000027dcccb: cmp    r12d,DWORD PTR [rbp+0xc]
  0x00000000027dcccf: je     0x00000000027dcce1  ;*goto
                                                ; - javaapplication27.TestThreadSafety$1::run@62 (line 31)
  0x00000000027dccd1: add    rbx,0x1            ; OopMap{rbp=Oop off=85}
                                                ;*goto
                                                ; - javaapplication27.TestThreadSafety$1::run@62 (line 31)
  0x00000000027dccd5: test   DWORD PTR [rip+0xfffffffffdb53325],eax        # 0x0000000000330000
                                                ;*goto
                                                ; - javaapplication27.TestThreadSafety$1::run@62 (line 31)
                                                ;   {poll}
  0x00000000027dccdb: jmp    0x00000000027dccd1
  0x00000000027dccdd: xor    ebp,ebp
  0x00000000027dccdf: jmp    0x00000000027dccc6
  0x00000000027dcce1: mov    edx,0xffffff86
  0x00000000027dcce6: mov    QWORD PTR [rsp+0x20],rbx
  0x00000000027dcceb: call   0x00000000027a90a0  ; OopMap{rbp=Oop off=112}
                                                ;*aload_0
                                                ; - javaapplication27.TestThreadSafety$1::run@2 (line 26)
                                                ;   {runtime_call}
  0x00000000027dccf0: int3   
  0x00000000027dccf1: mov    edx,0xffffffad
  0x00000000027dccf6: mov    QWORD PTR [rsp+0x20],rbx
  0x00000000027dccfb: call   0x00000000027a90a0  ; OopMap{rbp=Oop off=128}
                                                ;*aload_0
                                                ; - javaapplication27.TestThreadSafety$1::run@2 (line 26)
                                                ;   {runtime_call}
  0x00000000027dcd00: int3                      ;*aload_0
                                                ; - javaapplication27.TestThreadSafety$1::run@2 (line 26)
  0x00000000027dcd01: int3   
于 2013-08-27T14:56:16.283 回答
5

不,a != a不是线程安全的。该表达式由三部分组成:load a、load aagain 和 perform !=。另一个线程有可能获得对a的父级的内在锁定并a在 2 个加载操作之间更改 in 的值。

另一个因素是是否a是本地的。如果a是本地的,那么没有其他线程应该可以访问它,因此应该是线程安全的。

void method () {
    int a = 0;
    System.out.println(a != a);
}

也应该总是打印false.

声明aasvolatile并不能解决 if aisstatic或 instance 的问题。问题不在于线程具有不同的 值a,而是一个线程a以不同的值加载两次。它实际上可能会使情况降低线程安全性。如果a不是volatile,则a可能会被缓存,并且另一个线程中的更改不会影响缓存的值。

于 2013-08-27T13:12:15.057 回答
3

关于奇怪的行为:

由于变量a未标记为volatile,因此在某些时候它的值a可能会被线程缓存。两个asa != a都是缓存的版本,因此总是相同的(意思flag是现在总是false)。

于 2013-08-27T10:41:25.487 回答
0

即使是简单的读取也不是原子的。如果along且未标记为volatilethen 在 32 位 JVMlong b = a上不是线程安全的。

于 2013-09-23T08:20:56.497 回答