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()
在true
和false
设置之间引发exception,否则将永远不会打印Broke Synchronization
。 看到下面更好的模式。
编辑:
我把@卢克的代码,并削减到这个pastebin类 。 正如我所看到的,@ Luke正在碰到一个JRE同步错误。 我知道这很难相信,但我一直在看代码,我只是看不到问题。
既然你提到了ConcurrentModificationException
,我怀疑getBuffer()
在遍历整个list
时候会抛出它。 您发布的代码不应该由于同步而抛出ConcurrentModificationException
,但是我怀疑有一些额外的代码正在调用add
或remove
不同步的代码,或者当您在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”。
- 他们正在同步不同的对象(你说他们不是)
-
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的规范,如果虚拟机不尊重它,则会报错。