Java同步无法按预期工作

Luk*_*uke 37 java multithreading synchronization thread-safety

我有一个"简单"的4类示例,可以在多台计算机上可靠地显示java同步的意外行为.正如你可以在下面看到的那样,给定java sychronized关键字的契约,Broke Synchronization永远不应该从类TestBuffer中打印出来.

以下是将重现问题的4个类(至少对我而言).我对如何修复这个破碎的例子并不感兴趣,而是为什么它首先打破了.

同步问题 - Controller.java

同步问题 - SyncTest.java

同步问题 - TestBuffer.java

同步问题 - Tuple3f.java

这是我运行时获得的输出:

java -cp . SyncTest
Before Adding
Creating a TestBuffer
Before Remove
Broke Synchronization
1365192
Broke Synchronization
1365193
Broke Synchronization
1365194
Broke Synchronization
1365195
Broke Synchronization
1365196
Done
Run Code Online (Sandbox Code Playgroud)

更新:@Gray有一个最简单的例子,到目前为止.他的例子可以在这里找到:奇怪的JRC比赛条件

基于我从其他人那里获得的反馈,看起来问题可能发生在Windows 64和OSX上的Java 64位1.6.0_20-1.6.0_31(不确定更新的1.6.0)上.没有人能够在Java 7上重现该问题.它还可能需要多核机器来重现该问题.

原始问题:

我有一个类提供以下方法:

  • 删除 - 从列表中删除给定项目
  • getBuffer - 迭代列表中的所有项目

我把问题简化为下面的两个函数,这两个函数都在同一个对象中,它们都是synchronized.除非我弄错了,否则永远不应该打印"Broke Synchronization",因为在输入insideGetBuffer之前应始终将其设置为false remove.但是,在我的应用程序中,当我有1个线程调用重复删除而另一个重复调用getBuffer时,它正在打印"Broke Synchronization".症状是我得到了ConcurrentModificationException.

也可以看看:

非常奇怪的竞争条件,看起来像JRE问题

Sun Bug报告:

这被Sun确认为Java中的一个错误.它在jdk7u4中显然是固定的(不知不觉?),但他们没有将修复程序向后移植到jdk6. 错误ID:7176993

phi*_*lwb 17

我认为你确实在查看OSR中的JVM错误.使用@Gray中的简化程序(稍微修改以打印错误消息)和一些混乱/打印JIT编译的选项,您可以看到JIT发生了什么.并且,您可以使用一些选项来控制这个问题,从而可以抑制问题,这为JVM提供了大量证据.

运行方式:

java -XX:+PrintCompilation -XX:CompileThreshold=10000 phil.StrangeRaceConditionTest
Run Code Online (Sandbox Code Playgroud)

你可以得到一个错误条件(像其他大约80%的运行)和编译打印有点像:

 68   1       java.lang.String::hashCode (64 bytes)
 97   2       sun.nio.cs.UTF_8$Decoder::decodeArrayLoop (553 bytes)
104   3       java.math.BigInteger::mulAdd (81 bytes)
106   4       java.math.BigInteger::multiplyToLen (219 bytes)
111   5       java.math.BigInteger::addOne (77 bytes)
113   6       java.math.BigInteger::squareToLen (172 bytes)
114   7       java.math.BigInteger::primitiveLeftShift (79 bytes)
116   1%      java.math.BigInteger::multiplyToLen @ 138 (219 bytes)
121   8       java.math.BigInteger::montReduce (99 bytes)
126   9       sun.security.provider.SHA::implCompress (491 bytes)
138  10       java.lang.String::charAt (33 bytes)
139  11       java.util.ArrayList::ensureCapacity (58 bytes)
139  12       java.util.ArrayList::add (29 bytes)
139   2%      phil.StrangeRaceConditionTest$Buffer::<init> @ 38 (62 bytes)
158  13       java.util.HashMap::indexFor (6 bytes)
159  14       java.util.HashMap::hash (23 bytes)
159  15       java.util.HashMap::get (79 bytes)
159  16       java.lang.Integer::valueOf (32 bytes)
168  17 s     phil.StrangeRaceConditionTest::getBuffer (66 bytes)
168  18 s     phil.StrangeRaceConditionTest::remove (10 bytes)
171  19 s     phil.StrangeRaceConditionTest$Buffer::remove (34 bytes)
172   3%      phil.StrangeRaceConditionTest::strangeRaceConditionTest @ 36 (76 bytes)
ERRORS //my little change
219  15      made not entrant  java.util.HashMap::get (79 bytes)
Run Code Online (Sandbox Code Playgroud)

有三个OSR替换(编译ID上带有%批注的替换).我的猜测是它是第三个,它是调用remove()的循环,负责出错.这可以通过位于工作目录中的.hotspot_compiler文件从JIT中排除,其中包含以下内容:

exclude phil/StrangeRaceConditionTest strangeRaceConditionTest
Run Code Online (Sandbox Code Playgroud)

当您再次运行该程序时,您将获得此输出:

CompilerOracle: exclude phil/StrangeRaceConditionTest.strangeRaceConditionTest
 73   1       java.lang.String::hashCode (64 bytes)
104   2       sun.nio.cs.UTF_8$Decoder::decodeArrayLoop (553 bytes)
110   3       java.math.BigInteger::mulAdd (81 bytes)
113   4       java.math.BigInteger::multiplyToLen (219 bytes)
118   5       java.math.BigInteger::addOne (77 bytes)
120   6       java.math.BigInteger::squareToLen (172 bytes)
121   7       java.math.BigInteger::primitiveLeftShift (79 bytes)
123   1%      java.math.BigInteger::multiplyToLen @ 138 (219 bytes)
128   8       java.math.BigInteger::montReduce (99 bytes)
133   9       sun.security.provider.SHA::implCompress (491 bytes)
145  10       java.lang.String::charAt (33 bytes)
145  11       java.util.ArrayList::ensureCapacity (58 bytes)
146  12       java.util.ArrayList::add (29 bytes)
146   2%      phil.StrangeRaceConditionTest$Buffer::<init> @ 38 (62 bytes)
165  13       java.util.HashMap::indexFor (6 bytes)
165  14       java.util.HashMap::hash (23 bytes)
165  15       java.util.HashMap::get (79 bytes)
166  16       java.lang.Integer::valueOf (32 bytes)
174  17 s     phil.StrangeRaceConditionTest::getBuffer (66 bytes)
174  18 s     phil.StrangeRaceConditionTest::remove (10 bytes)
### Excluding compile: phil.StrangeRaceConditionTest::strangeRaceConditionTest
177  19 s     phil.StrangeRaceConditionTest$Buffer::remove (34 bytes)
324  15      made not entrant  java.util.HashMap::get (79 bytes)
Run Code Online (Sandbox Code Playgroud)

并且问题没有出现(至少不是在我做过的重复尝试中).

此外,如果稍微更改JVM选项,可能会导致问题消失.使用以下任一方法,我无法出现问题.

java -XX:+PrintCompilation -XX:CompileThreshold=100000 phil.StrangeRaceConditionTest
java -XX:+PrintCompilation -XX:FreqInlineSize=1 phil.StrangeRaceConditionTest
Run Code Online (Sandbox Code Playgroud)

有趣的是,这两者的编译输出仍然显示了删除循环的OSR.我的猜测(这是一个很大的问题)是通过编译阈值延迟JIT或更改FreqInlineSize会导致在这些情况下更改OSR处理,从而绕过您可能遇到的错误.

有关JVM选项的信息,请参见此处.

有关-XX:+ PrintCompilation输出的信息以及如何弄乱JIT的功能,请参见此处此处.


Gra*_*ray 10

因此,根据您发布的代码,Broke Synchronization除非getBuffer()truefalse设置之间抛出异常,否则永远不会打印.请参阅下面的更好的模式.

编辑:

我拿了@Luke的代码并将其削减到这个pastebin类.在我看来,@ Luke正在遇到一个JRE同步错误.我知道很难相信,但我一直在看代码,我只是看不出问题.


既然你提到了ConcurrentModificationException,我怀疑它getBuffer()是在它遍历时抛出它list.您发布的代码不应该抛出ConcurrentModificationException,因为同步的,但我怀疑,一些额外的代码调用add或者remove同步的,或当你在整个迭代要删除list.在迭代它时,您可以修改非同步集合的唯一方法是通过以下Iterator.remove()方法:

Iterator<Object> iterator = list.iterator();
while (iterator.hasNext()) {
   ...
   // it is ok to remove from the list this way while iterating
   iterator.remove();
}
Run Code Online (Sandbox Code Playgroud)

要保护您的标志,请务必在设置此类关键布尔值时使用try/finally.然后任何异常都会insideGetBuffer适当地恢复:

synchronized public Object getBuffer() {
    insideGetBuffer = true;
    try {
        int i=0;
        for(Object item : list) {
            i++;
        }
    } finally {
        insideGetBuffer = false;
    }
    return null;
}
Run Code Online (Sandbox Code Playgroud)

此外,它是一种更好的模式,可以围绕特定对象进行同步,而不是使用方法同步.如果你试图保护list,那么每次在该列表周围添加同步会更好.n

 synchronized (list) {
    list.remove();
 }
Run Code Online (Sandbox Code Playgroud)

您还可以将列表转换为同步列表,synchronize每次都不需要:

 List<Object> list = Collections.synchronizedList(new ArrayList<Object>());
Run Code Online (Sandbox Code Playgroud)