!= 检查线程安全吗?
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 将实际生成的代码类型。这里实际的“循环”是从0x27dccd1
到0x27dccdf
的三个指令。循环中的jmp
是无条件的(因为循环是无限的)。循环中仅有的另外两条指令是add rbc, 0x1
- 它正在递增countOfIterations
(尽管循环永远不会退出,因此不会读取该值:如果你闯入它,也许需要它调试器),...
... 和看起来很奇怪的 test
指令,它实际上只用于内存访问(注意 eax
甚至从未在方法中设置!):这是一个特殊的页面当 JVM 想要触发所有线程到达安全点时设置为不可读,因此它可以执行 gc 或其他需要所有线程处于已知状态的操作。
更重要的是,JVM 将instance. a != instance.a
比较完全提升到循环之外,并且在进入循环之前只执行一次!它知道不需要重新加载instance
或a
,因为它们没有被声明为 volatile 并且没有其他代码可以在同一个线程上更改它们,所以它只是假设它们在整个循环中是相同的,这是内存模型允许的。【参考方案6】:
不,a != a
不是线程安全的。这个表达式由三部分组成:加载a
,再次加载a
,执行!=
。另一个线程有可能获得a
的父级的内在锁定,并在两次加载操作之间更改a
的值。
另一个因素是a
是否是本地的。如果a
是本地的,则没有其他线程可以访问它,因此应该是线程安全的。
void method ()
int a = 0;
System.out.println(a != a);
也应该总是打印false
。
如果a
是static
或实例,将a
声明为volatile
并不能解决问题。问题不在于线程具有不同的a
值,而是一个线程使用不同的值两次加载a
。它实际上可能会使这种情况的线程安全性降低。如果a
不是volatile
,那么a
可能会被缓存,并且另一个线程中的更改不会影响缓存的值。
【讨论】:
您使用synchronized
的示例是错误的:为了保证打印false
的代码,所有set a
的方法都必须是synchronized
,也是。
为什么会这样?如果方法是同步的,那么任何其他线程如何在方法执行时获得a
的父级的内在锁定,需要设置值a
。
你的前提是错误的。您可以设置对象的字段而无需获取其内在锁。 Java 不需要线程在设置对象的字段之前获取对象的内在锁。【参考方案7】:
关于奇怪的行为:
由于变量a
没有被标记为volatile
,在某些时候它的值a
可能会被线程缓存。 a
s 和 a != a
都是缓存版本,因此总是相同的(意味着 flag
现在总是 false
)。
【讨论】:
【参考方案8】:即使是简单的读取也不是原子的。如果a
是long
并且没有标记为volatile
,那么在32 位JVM 上long b = a
不是线程安全的。
【讨论】:
易失性和原子性无关。即使我标记一个 volatile 它也将是非原子的 可变长字段的分配始终是原子的。 ++等其他操作则不然。以上是关于!= 检查线程安全吗?的主要内容,如果未能解决你的问题,请参考以下文章