Java同步不按预期工作

我有一个“简单”的4类示例,可靠地显示来自多个机器上的Java同步的意外行为。 正如你可以在下面看到的,给定java sychronized关键字的合同, Broke Synchronization不应该从类TestBuffer打印出来。

这里是四个类将重现这个问题(至less对我来说)。 我对如何解决这个破碎的例子不感兴趣,而是为什么它首先打破

同步问题 – 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 

更新:@格雷有迄今为止打破最简单的例子。 他的例子可以在这里find: 奇怪的JRC比赛条件

根据我从其他人处获得的反馈,在Windows和OSX上的Java 64位1.6.0_20-1.6.0_31(不确定更新的1.6.0)可能会出现此问题。 没有人能够重现Java 7上的问题。它可能还需要一个多核机器来重现这个问题。

原文问题:

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

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

我将问题简化为下面的两个函数,它们都在同一个对象中,并且都是synchronized 。 除非我错了,否则不应该打印“Broke Synchronization”,因为在inputremove之前,应始终将insideGetBuffer设置为false。 然而,在我的应用程序中,当我有1个线程调用重复删除,而另一个调用getBuffer反复打印“打破同步”。 症状是我得到一个ConcurrentModificationException

也可以看看:

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

Sun Bug报告:

这被Sun证实是Java的一个bug。 它在jdk7u4中显然是固定的(不知不觉中),但他们没有将修复移植到jdk6。 错误ID:7176993

我认为你确实在看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) 

有三个OSRreplace(在编译ID上带有%注释的replace)。 我的猜测是,它是第三个,这是调用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) 

而且这个问题不会出现(至less不是我所做过的重复尝试)。

另外,如果稍微更改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所做的事情。

因此,根据您发布的代码,除非getBuffer()truefalse设置之间引发exception,否则将永远不会打印Broke Synchronization 。 看到下面更好的模式。

编辑:

我把@卢克的代码,并削减到这个pastebin类 。 正如我所看到的,@ Luke正在碰到一个JRE同步错误。 我知道这很难相信,但我一直在看代码,我只是看不到问题。


既然你提到了ConcurrentModificationException ,我怀疑getBuffer()在遍历整个list时候会抛出它。 您发布的代码不应该由于同步而抛出ConcurrentModificationException ,但是我怀疑有一些额外的代码正在调用addremove 同步的代码,或者当您在list中迭代时remove代码。 在迭代它时,修改未同步集合的唯一方法是通过Iterator.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。 那么任何exception都会适当地恢复insideGetBuffer

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

而且,围绕特定对象进行同步而不是使用方法同步是一种更好的模式。 如果您试图保护list ,那么每次在list添加同步将会更好

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

您也可以将您的列表变成一个同步列表,您不必每次synchronize

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

根据这些代码,只有两种方法可以打印“Broke Synchronization”。

  1. 他们正在同步不同的对象(你说他们不是)
  2. insideGetBuffer被同步块外的另一个线程改变。

没有这两个,就不可能有一个你列出的代码将打印“Broke Synchronization”和ConcurrentModificationException 。 你能给出一小段可以运行的代码来certificate你在说什么吗?

更新:

我经历了卢克发布的例子,我看到Java 1.6_24-64位Windows上的奇怪行为。 在Remove方法中,TestBuffer的实例和insideGetBuffer的值是交替的。 请注意 ,字段未在同步区域之外更新。 只有一个TestBuffer实例,但让我们假设它们不是 – insideGetBuffer永远不会被设置为true(所以它必须是相同的实例)。

  synchronized public void remove(Object item) { boolean b = insideGetBuffer; if(insideGetBuffer){ System.out.println("Broke Synchronization : " + b + " - " + insideGetBuffer); } } 

有时它打印Broke Synchronization : true - false

我正在努力让汇编程序在Windows 64位Java上运行。

ConcurrentModificationException大部分时间不是由并发线程引起的。 这是由迭代的集合修改引起的:

 for (Object item : list) { if (someCondition) { list.remove(item); } } 

如果someCondition为true,上面的代码将导致ConcurrentModificationException。 迭代时,只能通过迭代器的方法修改集合:

 for (Iterator<Object> it = list.iterator(); it.hasNext(); ) { Object item = it.next(); if (someCondition) { it.remove(); } } 

我怀疑这是真正的代码中发生的事情。 发布的代码是好的。

你可以试试这个代码是一个自包含的testing吗?

 public static class TestBuffer { private final List<Object> list = new ArrayList<Object>(); private boolean insideGetBuffer = false; public TestBuffer() { System.out.println("Creating a TestBuffer"); } synchronized public void add(Object item) { list.add(item); } synchronized public void remove(Object item) { if (insideGetBuffer) { System.out.println("Broke Synchronization "); System.out.println(item); } list.remove(item); } synchronized public void getBuffer() { insideGetBuffer = true; // System.out.println("getBuffer."); try { int count = 0; for (int i = 0, listSize = list.size(); i < listSize; i++) { if (list.get(i) != null) count++; } } finally { // System.out.println(".getBuffer"); insideGetBuffer = false; } } } public static void main(String... args) throws IOException { final TestBuffer tb = new TestBuffer(); ExecutorService service = Executors.newCachedThreadPool(); final AtomicLong count = new AtomicLong(); for (int i = 0; i < 16; i++) { final int finalI = i; service.submit(new Runnable() { @Override public void run() { while (true) { for (int j = 0; j < 1000000; j++) { tb.add(finalI); tb.getBuffer(); tb.remove(finalI); } System.out.printf("%d,: %,d%n", finalI, count.addAndGet(1000000)); } } }); } } 

版画

 Creating a TestBuffer 11,: 1,000,000 2,: 2,000,000 ... many deleted ... 2,: 100,000,000 1,: 101,000,000 

更详细地看你的堆栈跟踪。

 Caused by: java.util.ConcurrentModificationException at java.util.HashMap$HashIterator.nextEntry(Unknown Source) at java.util.HashMap$KeyIterator.next(Unknown Source) at <removed>.getBuffer(<removed>.java:62) 

你可以看到你正在访问一个HashMap的键集,而不是一个列表。 这一点很重要,因为密钥集是基础地图上的一个视图 。 这意味着您需要确保每个对该地图的访问都受到同一个锁的保护。 比如说你有一个像二传手

 Collection list; public void setList(Collection list) { this.list = list; } // somewhere else Map map = new HashMap(); obj.setList(map.keySet()); // "list" is accessed in another thread which is locked by this thread does this map.put("hello", "world"); // now an Iterator in another thread on list is invalid. 

Controller类中的'getBuffer'函数正在创build这个问题。 如果两个线程第一次同时进入下面的“if”条件,那么控制器将最终创build两个缓冲区对象。 在第一个对象上调用add函数,在第二个对象上调用remove。

 if (colorToBufferMap.containsKey(defaultColor)) { 

当两个线程(添加和删除线程)同时进入(当缓冲区尚未添加到colorToBufferMap时),上面的if语句将返回false,并且这两个线程将进入else部分并创build两个缓冲区,因为缓冲区是局部variables这两个线程将会收到两个不同的缓冲区实例作为return语句的一部分。 但是,只有最后创build的一个将被存储在全局variables“colorToBufferMap”中。

上面有问题的行是getBuffer函数的一部分

 public TestBuffer getBuffer() { TestBuffer buffer = null; if (colorToBufferMap.containsKey(defaultColor)) { buffer = colorToBufferMap.get(defaultColor); } else { buffer = new TestBuffer(); colorToBufferMap.put(defaultColor, buffer); } return buffer; } 

在Controller类中同步'getBuffer'函数可以解决这个问题。

编辑:只有当两个不同的对象实例用于重复调用方法时,答案才有效。

场景:你有两个同步的方法。 一个用于删除一个实体,另一个用于访问。 当1个线程在remove方法中,另一个线程在getBuffer方法中并设置了insideGetBuffer = true时,问题就出现了。

当你发现你需要在列表上同步,因为这两个方法在你的列表上工作。

如果访问list和insideGetBuffer完全包含在代码中,那么代码看起来确实是线程安全的,除非出现JVM错误,否则我不会看到“打破同步”的可能性。

你可以仔细检查所有可能的访问你的成员variables(列表和insideGetBuffer)? 可能性包括列表是否通过构造函数传递给您(您的代码不会显示),或者这些variables是受保护的variables,因此子类可以更改它们。

另一种可能性是通过反思访问。

我不相信这是JVM中的一个错误。

我的第一个怀疑是,这是编译器正在做某种操作重新sorting(在我的机器上,它在debugging器中工作正常,但运行时同步失败),但

我不能告诉你为什么,但我非常强烈地怀疑,有些东西放弃了TestBuffer上的隐含声明getBuffer()和remove(…)同步的锁。

例如,将其replace为:

 public void getBuffer() { synchronized (this) { this.insideGetBuffer = true; try { int i = 0; for (Object item : this.list) { if (item != null) { i++; } } } finally { this.insideGetBuffer = false; } } } public void remove(final Object item) { synchronized (this) { // fails if this is called while getBuffer is running if (this.insideGetBuffer) { System.out.println("Broke Synchronization "); System.out.println(item); } } } 

而你仍然有你的同步错误。 但select其他的东西login,例如:

 private Object lock = new Object(); public void getBuffer() { synchronized (this.lock) { this.insideGetBuffer = true; try { int i = 0; for (Object item : this.list) { if (item != null) { i++; } } } finally { this.insideGetBuffer = false; } } } public void remove(final Object item) { synchronized (this.lock) { // fails if this is called while getBuffer is running if (this.insideGetBuffer) { System.out.println("Broke Synchronization "); System.out.println(item); } } } 

一切都如预期般运作。

现在,您可以通过添加以下内容来模拟放弃locking:

 this.lock.wait(1); 

在getBuffer()的for循环中,你将再次失败。

我仍然坚持放弃locking,但总的来说,使用受保护locking的显式同步可能比同步操作符更好。

我以前有过类似的问题。 错误是你没有声明一些字段为volatile 。 这个关键字用于表示一个字段将被不同的线程修改,因此不能被caching。 相反,所有的写入操作和读取操作都必须进入字段的“真实”存储位置。

欲了解更多信息,只需谷歌“ Java内存模型 ”

虽然大部分读者都关注类TestBuffer ,但我认为问题可能在其他地方(例如,你是否试图在类控制器上添加同步?或者使其字段变得不稳定)。

PS。 注意到不同的java虚拟机可能会在不同的平台上使用不同的优化,因此在一个平台上可能会出现更多的同步问题。 要保证安全的唯一方法就是遵守Java的规范,如果虚拟机不尊重它,则会报错。