Java同步无法按预期工作

时间:2012-06-11 15:16:31

标签: java multithreading synchronization thread-safety

我有一个“简单”的4类示例,可以在多台计算机上可靠地显示java同步的意外行为。如下所示,给定java sychronized关键字的约定,永远不应该从TestBuffer类打印Broke Synchronization

以下是将重现问题的4个类(至少对我而言)。我对如何修复这个破碎的例子并不感兴趣,而是首先为什么它会破坏

Sync Issue - Controller.java

Sync Issue - SyncTest.java

Sync Issue - TestBuffer.java

Sync Issue - 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上重现这个问题。它可能还需要一台多核机器来重现这个问题。

原始问题:

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

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

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

另见:

Very strange race condition which looks like a JRE issue

Sun Bug报告:

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

10 个答案:

答案 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

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

答案 1 :(得分:10)

因此,根据您发布的代码,除非Broke SynchronizationgetBuffer()true设置之间引发异常,否则您永远不会打印false。请参阅下面的更好的模式。

修改

我拿了@Luke的代码并将其缩小到this pastebin class。在我看来,@ Luke正在遇到一个JRE同步错误。我知道很难相信,但我一直在看代码,我只是看不出问题。


由于您提及ConcurrentModificationException,我怀疑getBuffer()list上进行迭代时会抛出它。由于同步原因,您发布的代码永远不会抛出ConcurrentModificationException,但我怀疑某些其他代码正在调用同步的addremove,或者当您在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”。

  1. 他们正在同步不同的对象(你说它们不是)
  2. insideGetBuffer正被同步块之外的另一个线程更改。
  3. 如果没有这两个,那么您列出的代码将无法打印“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)

Controller类中的

'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不尊重它,则提出错误。