!= 检查线程安全吗?

Posted

技术标签:

【中文标题】!= 检查线程安全吗?【英文标题】:Is the != check thread safe? 【发布时间】:2013-08-29 21:56:06 【问题描述】:

我知道像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

【问题讨论】:

在这种情况下,“线程安全”是什么意思?你是在问它是否保证总是返回false? @JBNizet 是的。我就是这么想的。 在单线程上下文中它甚至不总是返回 false。它可能是一个 NaN.. 可能的解释:代码是即时编译的,编译后的代码只加载一次变量。这是意料之中的。 打印单个结果是测试比赛的糟糕方式。与您的测试相比,打印(格式化和写入结果)成本相对较高(有时当与终端的连接或终端本身的带宽很慢时,您的程序最终会阻塞写入)。此外,IO 通常包含自己的互斥锁,它们会改变线程的执行顺序(注意1234:true 的各个行从不粉碎彼此)。比赛测试需要更紧密的内循环。在最后打印一个摘要(就像下面有人使用单元测试框架所做的那样)。 【参考方案1】:

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

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 所做的更改将对当前线程可见。

【讨论】:

虽然证据确凿,但字节码实际上并不是证据。它也必须在 JLS 中的某个地方...... @Marko 我同意你的想法,但不一定是你的结论。对我来说,上面的字节码是实现!= 的明显/规范方式,它涉及分别加载 LHS 和 RHS。因此,如果 JLS 没有在 LHS 和 RHS 语法相同时提及任何有关优化的具体内容,则将适用一般规则,这意味着加载 a 两次。 其实假设生成的字节码符合JLS,就是一个证明! @Adrian: 首先:即使那个假设是无效的,一个 single 编译器的存在,它可以评估为“真”足以证明它有时可以评估为“真”(即使规范禁止它——它没有)。其次:Java 规范明确,大多数编译器都严格遵守它。在这方面将它们用作参考是有意义的。第三:您使用“JRE”一词,但我认为它并不意味着您认为它的含义。 . . @AlexanderTorstling - “我不确定这是否足以排除单次读取优化。” 这还不够。事实上,在没有同步的情况下(以及强加的额外“发生在”关系),优化是有效的,【参考方案2】:

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

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

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

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

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

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

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


关于“奇怪的行为”:

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

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

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

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

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

请注意,这所有都与 JLS 允许 Java 实现执行的操作一致。


@kriss 如此评论:

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

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

只要代码的最终效果相同,编译器就可以合并和重新排序加载和保存(以及做其他事情):

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

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

【讨论】:

这是否意味着a != a 可以返回true? 我的意思是,也许在我的机器上我无法模拟上面的代码是非线程安全的。所以也许它背后有一个理论推理。 @NarendraPathai - 没有理论上的理由不能证明它。可能有一个实际的原因......或者也许你只是没有走运。 请用我正在使用的程序检查我的更新答案。检查有时会返回 true,但输出中似乎有一种奇怪的行为。 @NarendraPathai - 看我的解释。【参考方案3】:

用 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 次失败。所以NO,它是NO线程安全的

【讨论】:

您甚至没有检查是否相等...Random.nextInt() 部分是多余的。您也可以使用 new Object() 进行测试。 @MarkoTopolnik 请用我正在使用的程序检查我的更新答案。检查有时会返回 true,但输出中似乎有一种奇怪的行为。 附注,随机对象通常是为了重复使用,而不是在每次需要新 int 时创建。【参考方案4】:

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

Java VM 可以:

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

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

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

如果你想确定变量是如何被访问的,你必须使它成为volatile(所谓的“半内存屏障”)或添加一个完整的内存屏障(synchronized)。您还可以使用一些 hgiher 级别的 API(例如 Juned Ahasan 提到的 AtomicInteger)。

有关线程安全的详细信息,请阅读JSR 133 (Java Memory Model)。

【讨论】:

a 声明为volatile 仍然意味着两个不同的读取,并且可能在两者之间发生变化。【参考方案5】:

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            ; OopMaprbp=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  ; OopMaprbp=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  ; OopMaprbp=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   

【讨论】:

这是一个很好的例子,当你有一个无限循环并且所有东西都可以或多或少地被提升时,JVM 将实际生成的代码类型。这里实际的“循环”是从0x27dccd10x27dccdf 的三个指令。循环中的jmp 是无条件的(因为循环是无限的)。循环中仅有的另外两条指令是add rbc, 0x1 - 它正在递增countOfIterations(尽管循环永远不会退出,因此不会读取该值:如果你闯入它,也许需要它调试器),... ... 和看起来很奇怪的 test 指令,它实际上只用于内存访问(注意 eax 甚至从未在方法中设置!):这是一个特殊的页面当 JVM 想要触发所有线程到达安全点时设置为不可读,因此它可以执行 gc 或其他需要所有线程处于已知状态的操作。 更重要的是,JVM 将instance. a != instance.a 比较完全提升到循环之外,并且在进入循环之前只执行一次!它知道不需要重新加载instancea,因为它们没有被声明为 volatile 并且没有其他代码可以在同一个线程上更改它们,所以它只是假设它们在整个循环中是相同的,这是内存模型允许的。【参考方案6】:

不,a != a 不是线程安全的。这个表达式由三部分组成:加载a,再次加载a,执行!=。另一个线程有可能获得a 的父级的内在锁定,并在两次加载操作之间更改a 的值。

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

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

也应该总是打印false

如果astatic 或实例,将a 声明为volatile 并不能解决问题。问题不在于线程具有不同的a 值,而是一个线程使用不同的值两次加载a。它实际上可能会使这种情况的线程安全性降低。如果a 不是volatile,那么a 可能会被缓存,并且另一个线程中的更改不会影响缓存的值。

【讨论】:

您使用synchronized 的示例是错误的:为了保证打印false 的代码,所有set a 的方法都必须是synchronized ,也是。 为什么会这样?如果方法是同步的,那么任何其他线程如何在方法执行时获得a 的父级的内在锁定,需要设置值a 你的前提是错误的。您可以设置对象的字段而无需获取其内在锁。 Java 不需要线程在设置对象的字段之前获取对象的内在锁。【参考方案7】:

关于奇怪的行为:

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

【讨论】:

【参考方案8】:

即使是简单的读取也不是原子的。如果along 并且没有标记为volatile,那么在32 位JVM 上long b = a 不是线程安全的。

【讨论】:

易失性和原子性无关。即使我标记一个 volatile 它也将是非原子的 可变长字段的分配始终是原子的。 ++等其他操作则不然。

以上是关于!= 检查线程安全吗?的主要内容,如果未能解决你的问题,请参考以下文章

SecureRandom 线程安全吗?

单例模式双重检查(DCL)引发的多线程问题

Files.copy 是 Java 中的线程安全函数吗?

errno 是线程安全的吗?

c++ string线程安全吗

java priorityblockingqueue 线程安全吗