37

我有一个“简单”的 4 类示例,它可靠地显示了多台机器上 java 同步的意外行为。正如您在下面看到的,鉴于 javasychronized关键字的约定,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

更新:@Gray 有迄今为止最简单的例子。他的例子可以在这里找到:Strange JRC Race Condition

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

原始问题:

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

  • remove - 从列表中删除给定项目
  • getBuffer - 遍历列表中的所有项目

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

也可以看看:

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

太阳错误报告:

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

4

10 回答 10

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 选项的信息,请参见此处

有关-XX:+PrintCompilation 的输出以及如何弄乱 JIT 所做的事情的信息,请参见此处此处。

于 2012-06-15T17:12:07.620 回答
10

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

编辑:

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


既然你提到ConcurrentModificationException了,我怀疑getBuffer()它在遍历list. 您发布的代码不应该ConcurrentModificationException因为同步而抛出 a ,但我怀疑一些额外的代码正在调用或未add同步,或者您在遍历. 在迭代时修改未同步集合的唯一方法是通过以下方法:removelistIterator.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>());
于 2012-06-11T15:20:53.840 回答
4

基于该代码,“中断同步”只有两种打印方式。

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

如果没有这两个,您列出的代码将无法打印“中断同步”和ConcurrentModificationException. 你能给出一小段代码来证明你在说什么吗?

更新:

我浏览了 Luke 发布的示例,我在 Java 1.6_24-64 位 Windows 上看到了奇怪的行为。TestBuffer 的相同实例和值insideGetBuffer在 remove 方法中是“交替的”。请注意,该字段不会在同步区域之外更新。只有一个 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 上运行。

于 2012-06-11T15:20:05.493 回答
2

大多数情况下,ConcurrentModificationException 不是由并发线程引起的。它是由在迭代时修改集合引起的:

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

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

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

我怀疑这就是您的真实代码中发生的情况。发布的代码很好。

于 2012-06-11T15:21:59.037 回答
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.
于 2012-06-12T07:27:16.947 回答
2

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

if (colorToBufferMap.containsKey(defaultColor)) {

当两个线程(添加和删除线程)同时进入时(缓冲区尚未添加到 colorToBufferMap 时),上面的 if 语句将返回 false,两个线程都会进入 else 部分并创建两个缓冲区,因为缓冲区是一个这两个线程的局部变量将接收两个不同的缓冲区实例作为返回语句的一部分。但是,只有最后一个创建的会存储在全局变量“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”函数将解决此问题。

于 2012-06-14T14:07:03.573 回答
1

编辑:仅当在重复调用方法时使用两个不同的 Object 实例时,答案才有效。

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

正如您发现的那样,您需要将同步放在列表中,因为这两种方法都适用于您的列表。

于 2012-06-11T15:29:33.367 回答
1

如果对 list 和 insideGetBuffer 的访问完全包含在该代码中,则代码看起来肯定是线程安全的,并且我看不到可以打印“中断同步”的可能性,除非出现 JVM 错误。

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

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

于 2012-06-12T16:16:09.623 回答
1

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

我的第一个怀疑是编译器正在执行某种重新排序操作(在我的机器上,它在调试器中运行良好,但运行时同步失败)但是

我不能告诉你为什么,但我非常怀疑某些东西正在放弃对 TestBuffer 的锁定,这在声明 getBuffer() 和 remove(...) 同步时是隐含的。

例如,将它们替换为:

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 循环中,您将再次开始失败。

我仍然对放弃锁的原因感到困惑,但总的来说,在受保护的锁上使用显式同步可能比同步运算符更好

于 2012-06-13T22:32:12.247 回答
0

我之前也遇到过类似的问题。错误是您没有将某些字段声明为volatile. 该关键字用于表示一个字段将被不同的线程修改,因此它不能被缓存。相反,所有写入和读取都必须转到字段的“真实”内存位置。

有关更多信息,请在 Google 上搜索“ Java 内存模型

尽管大多数读者都关注 class TestBuffer,但我认为问题可能出在其他地方(例如,您是否尝试在类 Controller 上添加同步?或者将其字段设置为 volatile ?)。

附言。请注意,不同的 java VM 可能在不同的平台上使用不同的优化,因此同步问题在一个平台上可能比另一个平台上出现的频率更高。唯一安全的方法是遵守 Java 规范,如果 VM 不遵守它,则提交错误。

于 2012-06-20T08:16:38.310 回答