我有一个“简单”的4类示例,可以在多台计算机上可靠地显示java同步的意外行为。如下所示,给定java sychronized
关键字的约定,永远不应该从TestBuffer类打印Broke Synchronization
。
以下是将重现问题的4个类(至少对我而言)。我对如何修复这个破碎的例子并不感兴趣,而是首先为什么它会破坏。
这是我运行时获得的输出:
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有一个最简单的例子,到目前为止。他的例子可以在这里找到:Strange JRC Race Condition
根据我从其他人那里得到的反馈,看起来问题可能发生在Windows和OSX上的Java 64位1.6.0_20-1.6.0_31(不确定更新的1.6.0)上。没有人能够在Java 7上重现这个问题。它可能还需要一台多核机器来重现这个问题。
原始问题:
我有一个提供以下方法的课程:
我已将问题简化为下面的两个函数,两个函数都在同一个对象中,它们都是synchronized
。除非我弄错了,否则永远不应打印“Broke Synchronization”,因为在insideGetBuffer
输入之前remove
应始终设置为false。但是,在我的应用程序中,当我有1个线程调用重复删除而另一个重复调用getBuffer时,它正在打印“Broke Synchronization”。症状是我得到ConcurrentModificationException
。
Very strange race condition which looks like a JRE issue
这被Sun确认为Java中的一个错误。它在jdk7u4中显然是固定的(不知不觉?),但是他们还没有将修复程序向后移植到jdk6。 Bug ID: 7176993
答案 0 :(得分:17)
我认为您确实在查看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选项的信息,请参阅here。
答案 1 :(得分:10)
因此,根据您发布的代码,除非Broke Synchronization
在getBuffer()
和true
设置之间引发异常,否则您永远不会打印false
。请参阅下面的更好的模式。
修改强>
我拿了@Luke的代码并将其缩小到this pastebin class。在我看来,@ 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();
}
要保护您的标志,请务必在设置此类关键布尔值时使用try / finally。然后任何异常都会适当地恢复insideGetBuffer
:
synchronized public Object getBuffer() {
insideGetBuffer = true;
try {
int i=0;
for(Object item : list) {
i++;
}
} finally {
insideGetBuffer = false;
}
return null;
}
此外,它是一种更好的模式,可以围绕特定对象进行同步,而不是使用方法同步。如果您尝试保护list
,那么每次在该列表周围添加同步会更好.n
synchronized (list) {
list.remove();
}
您还可以将列表转换为同步列表,每次都不需要synchronize
:
List<Object> list = Collections.synchronizedList(new ArrayList<Object>());
答案 2 :(得分:4)
根据该代码,只有两种方法可以打印“Broke Synchronization”。
insideGetBuffer
正被同步块之外的另一个线程更改。如果没有这两个,那么您列出的代码将无法打印“Broke Synchronization”&amp; ConcurrentModificationException
。你能提供一小段代码可以运行来证明你在说什么吗?
<强>更新强>
我查看了Luke发布的示例,我看到Java 1.6_24-64位Windows上的奇怪行为。 TestBuffer的相同实例和insideGetBuffer
的值在remove方法中是'alternate'。 注意该字段未在同步区域外更新。只有一个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上运行。
答案 3 :(得分:2)
大多数情况下,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();
}
}
我怀疑这是你真实代码中发生的事情。发布的代码很好。
答案 4 :(得分:2)
你可以尝试这个自包含测试的代码吗?
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.
答案 5 :(得分:2)
'getBuffer'函数正在创建此问题。如果两个线程第一次同时进入以下'if'条件,则控制器将最终创建两个缓冲区对象。在第一个对象上调用add函数,在第二个对象上调用remove。
if (colorToBufferMap.containsKey(defaultColor)) {
当两个线程(添加和删除线程)同时进入时(当缓冲区尚未添加到colorToBufferMap时),上面的if语句将返回false并且两个线程都将进入else部分并创建两个缓冲区,因为buffer是一个局部变量,这两个线程将接收两个不同的缓冲区实例作为return语句的一部分。但是,只有最后创建的一个将存储在全局变量'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'函数将解决此问题。
答案 6 :(得分:1)
编辑:只有在重复调用方法时使用两个不同的Object实例时,才会生效。
场景: 你有两个同步方法。一个用于删除实体,另一个用于访问。 当1个线程在remove方法中而另一个线程在getBuffer方法中时,会出现问题 设置insideGetBuffer = true。
正如您所发现的那样,您需要在列表中放置同步,因为这些方法都适用于您的列表。
答案 7 :(得分:1)
如果该代码中完全包含对list和insideGetBuffer的访问权限,那么代码看起来肯定是线程安全的,并且我没有看到可能打印“Broke synchronization”的可能性,除非出现JVM错误。
您可以仔细检查对您的成员变量(list和insideGetBuffer)的所有可能访问权限吗?可能性包括列表是否通过构造函数(您的代码未显示)传递给您,或者这些变量是受保护的变量,因此子类可以更改它们。
另一种可能性是通过反思进行访问。
答案 8 :(得分:1)
我不相信这是JVM中的错误。
我的第一个怀疑是,这是某种操作重新排序编译器正在进行的操作(在我的机器上,它在调试器中工作正常,但在运行时同步失败)但是
我无法告诉你为什么,但我非常强烈怀疑某些事情正在放弃TestBuffer上的锁定,这是在声明getBuffer()和remove(...)synchronized时隐含的。
例如,将其替换为:
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);
}
}
}
你仍然有同步错误。但是选择其他东西登录,例如:
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);
}
}
}
一切都按预期工作。
现在,您可以通过添加以下内容来模拟放弃锁定:
this.lock.wait(1);
在getBuffer()的for循环中,你将再次开始失败。
我仍然对放弃锁定的内容感到困惑,但一般来说,在受保护的锁上使用显式同步比同步操作符更好。
答案 9 :(得分:0)
之前我遇到过类似的问题。错误是您没有将某个字段声明为volatile
。此关键字用于指示字段将由不同的线程修改,因此无法缓存。相反,所有写入和读取必须转到该字段的“真实”存储位置。
有关详细信息,只需谷歌“Java Memory Model”
虽然大多数读者都关注课程TestBuffer
,但我认为问题可能出在其他地方(例如,您是否尝试在类控制器上添加同步?或者将其字段设为volatile?)。
PS。请注意,不同的Java VM可能在不同的平台中使用不同的优化,因此同步问题可能更多地出现在平台上而不是另一个平台上。保证安全的唯一方法是遵守Java的规范,如果VM不尊重它,则提出错误。