Java 同步未按预期工作

我有一个“简单”的4类示例,它可靠地显示了多台计算机上java同步的意外行为。正如你在下面读到的,给定java关键字的契约,永远不应该从类TestBuffer打印出来。sychronizedBroke Synchronization

以下是将重现该问题的4个类(至少对我来说)。我对如何修复这个损坏的示例不感兴趣,而是对为什么它首先会中断感兴趣。

同步问题 - 控制器.java

同步问题 - 同步测试.java

同步问题 - 测试缓冲区.java

同步问题 - 元组3f.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

更新:@Gray有迄今为止最简单的例子。他的例子可以在这里找到:奇怪的JRC竞争条件

根据我从其他人那里得到的反馈,看起来这个问题可能会发生在Windows和OSX上的Java 64位1.6.0_20-1.6.0_31(不确定更新的1.6.0)上。没有人能够在Java 7上重现这个问题。它还可能还需要多核计算机才能重现问题。

原始问题:

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

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

我将问题简化为下面的2个函数,它们都位于同一对象中,并且它们都是。除非我弄错了,否则永远不应该打印“中断的同步”,因为在可以输入之前,应该始终将其设置回 false。但是,在我的应用程序中,当我有1个线程重复调用delete而其他调用getBuffer时,它会打印“中断同步”。症状是我得到一个.synchronizedinsideGetBufferremoveConcurrentModificationException

另请参阅:

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

太阳错误报告:

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


答案 1

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

运行方式:

java -XX:+PrintCompilation -XX:CompileThreshold=10000 phil.StrangeRaceConditionTest

你可以得到一个错误条件(像其他人一样,大约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)

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

exclude phil/StrangeRaceConditionTest strangeRaceConditionTest

再次运行该程序时,将得到以下输出:

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)

并且问题没有出现(至少在我所做的反复尝试中没有出现)。

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

java -XX:+PrintCompilation -XX:CompileThreshold=100000 phil.StrangeRaceConditionTest
java -XX:+PrintCompilation -XX:FreqInlineSize=1 phil.StrangeRaceConditionTest

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

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

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


答案 2

因此,根据您发布的代码,除非在 和 设置之间引发异常,否则您将永远不会被打印出来。在下面看到更好的模式。Broke SynchronizationgetBuffer()truefalse

编辑:

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


既然你提到,我怀疑当它迭代穿过.您发布的代码永远不应该因为同步而抛出a,但我怀疑某些其他代码正在调用或同步,或者您在迭代.在循环访问未同步的集合时,修改该集合的唯一方法是使用以下方法:ConcurrentModificationExceptiongetBuffer()listConcurrentModificationExceptionaddremovelistIterator.remove()

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

为了保护您的标志,请确保在设置这样的关键布尔值时使用 try/finally。然后,任何异常都会相应地恢复:insideGetBuffer

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

此外,在特定对象周围进行同步而不是使用方法同步是一种更好的模式。如果您尝试保护 ,则每次都围绕该列表添加同步会更好。list

 synchronized (list) {
    list.remove();
 }

您还可以将列表转换为同步列表,而不必每次都打开它:synchronize

 List<Object> list = Collections.synchronizedList(new ArrayList<Object>());