Java 中使用同步块的并发未给出预期结果
Concurrency in Java using synchronized blocks not giving expected results
下面是一个简单的 java 程序。它有一个名为 "cnt" 的计数器,该计数器会递增,然后添加到名为 "monitor" 的列表中。 "cnt"被多线程自增,"monitor"被多线程自增
在方法"go()"的最后,cnt和monitor.size()应该有相同的值,但他们没有。 monitor.size() 确实有正确的值。
如果您通过取消注释其中一个已注释的同步块并注释掉当前未注释的块来更改代码,则代码会产生预期的结果。此外,如果将线程数 (THREAD_COUNT) 设置为 1,代码会产生预期的结果。
这只能在具有多个真实内核的机器上重现。
public class ThreadTester {
private List<Integer> monitor = new ArrayList<Integer>();
private Integer cnt = 0;
private static final int NUM_EVENTS = 2313;
private final int THREAD_COUNT = 13;
public ThreadTester() {
}
public void go() {
Runnable r = new Runnable() {
@Override
public void run() {
for (int ii=0; ii<NUM_EVENTS; ++ii) {
synchronized( monitor) {
synchronized(cnt) { // <-- is this synchronized necessary?
monitor.add(cnt);
}
// synchronized(cnt) {
// cnt++; // <-- why does moving the synchronized block to here result in the correct value for cnt?
// }
}
synchronized(cnt) {
cnt++; // <-- why does moving the synchronized block here result in cnt being wrong?
}
}
// synchronized(cnt) {
// cnt += NUM_EVENTS; // <-- moving the synchronized block here results in the correct value for cnt, no surprise
// }
}
};
Thread[] threads = new Thread[THREAD_COUNT];
for (int ii=0; ii<THREAD_COUNT; ++ii) {
threads[ii] = new Thread(r);
}
for (int ii=0; ii<THREAD_COUNT; ++ii) {
threads[ii].start();
}
for (int ii=0; ii<THREAD_COUNT; ++ii) {
try { threads[ii].join(); } catch (InterruptedException e) { }
}
System.out.println("Both values should be: " + NUM_EVENTS*THREAD_COUNT);
synchronized (monitor) {
System.out.println("monitor.size() " + monitor.size());
}
synchronized (cnt) {
System.out.println("cnt " + cnt);
}
}
public static void main(String[] args) {
ThreadTester t = new ThreadTester();
t.go();
System.out.println("DONE");
}
}
好的,让我们看看您提到的不同可能性:
1.
for (int ii=0; ii<NUM_EVENTS; ++ii) {
synchronized( monitor) {
synchronized(cnt) { // <-- is this synchronized necessary?
monitor.add(cnt);
}
synchronized(cnt) {
cnt++; // <-- why does moving the synchronized block to here result in the correct value for cnt?
}
}
首先,monitor 对象在线程之间共享,因此锁定它(这就是 synchronized 所做的)将确保块内的代码一次只能由一个线程执行。所以外层里面的2个synchronized是没有必要的,反正代码是受保护的。
2.
for (int ii=0; ii<NUM_EVENTS; ++ii) {
synchronized( monitor) {
monitor.add(cnt);
}
synchronized(cnt) {
cnt++; // <-- why does moving the synchronized block here result in cnt being wrong?
}
}
好的,这个有点棘手。 cnt 是一个 Integer 对象并且 Java 不允许修改 Integer 对象(Integers 是不可变的),即使代码表明这就是这里发生的事情。但实际上会发生的是 cnt++ 将创建一个值为 cnt + 1 的新 Integer 并覆盖 cnt。
这就是代码的实际作用:
synchronized(cnt) {
Integer tmp = new Integer(cnt + 1);
cnt = tmp;
}
问题是当一个线程将创建一个新的 cnt 对象时,所有其他线程都在等待获取对旧对象的锁定。该线程现在释放旧的 cnt,然后将尝试获取新的 cnt 对象的锁并获取它,同时另一个线程获取旧的 cnt 对象的锁。突然有 2 个线程在临界区,执行相同的代码并导致竞争条件。这就是错误结果的来源。
如果您删除第一个同步块(带监视器的块),那么您的结果会变得更加错误,因为竞争的机会会增加。
一般来说,您应该尝试仅对 final 变量使用 synchronized 以防止这种情况发生。
下面是一个简单的 java 程序。它有一个名为 "cnt" 的计数器,该计数器会递增,然后添加到名为 "monitor" 的列表中。 "cnt"被多线程自增,"monitor"被多线程自增
在方法"go()"的最后,cnt和monitor.size()应该有相同的值,但他们没有。 monitor.size() 确实有正确的值。
如果您通过取消注释其中一个已注释的同步块并注释掉当前未注释的块来更改代码,则代码会产生预期的结果。此外,如果将线程数 (THREAD_COUNT) 设置为 1,代码会产生预期的结果。
这只能在具有多个真实内核的机器上重现。
public class ThreadTester {
private List<Integer> monitor = new ArrayList<Integer>();
private Integer cnt = 0;
private static final int NUM_EVENTS = 2313;
private final int THREAD_COUNT = 13;
public ThreadTester() {
}
public void go() {
Runnable r = new Runnable() {
@Override
public void run() {
for (int ii=0; ii<NUM_EVENTS; ++ii) {
synchronized( monitor) {
synchronized(cnt) { // <-- is this synchronized necessary?
monitor.add(cnt);
}
// synchronized(cnt) {
// cnt++; // <-- why does moving the synchronized block to here result in the correct value for cnt?
// }
}
synchronized(cnt) {
cnt++; // <-- why does moving the synchronized block here result in cnt being wrong?
}
}
// synchronized(cnt) {
// cnt += NUM_EVENTS; // <-- moving the synchronized block here results in the correct value for cnt, no surprise
// }
}
};
Thread[] threads = new Thread[THREAD_COUNT];
for (int ii=0; ii<THREAD_COUNT; ++ii) {
threads[ii] = new Thread(r);
}
for (int ii=0; ii<THREAD_COUNT; ++ii) {
threads[ii].start();
}
for (int ii=0; ii<THREAD_COUNT; ++ii) {
try { threads[ii].join(); } catch (InterruptedException e) { }
}
System.out.println("Both values should be: " + NUM_EVENTS*THREAD_COUNT);
synchronized (monitor) {
System.out.println("monitor.size() " + monitor.size());
}
synchronized (cnt) {
System.out.println("cnt " + cnt);
}
}
public static void main(String[] args) {
ThreadTester t = new ThreadTester();
t.go();
System.out.println("DONE");
}
}
好的,让我们看看您提到的不同可能性:
1.
for (int ii=0; ii<NUM_EVENTS; ++ii) {
synchronized( monitor) {
synchronized(cnt) { // <-- is this synchronized necessary?
monitor.add(cnt);
}
synchronized(cnt) {
cnt++; // <-- why does moving the synchronized block to here result in the correct value for cnt?
}
}
首先,monitor 对象在线程之间共享,因此锁定它(这就是 synchronized 所做的)将确保块内的代码一次只能由一个线程执行。所以外层里面的2个synchronized是没有必要的,反正代码是受保护的。
2.
for (int ii=0; ii<NUM_EVENTS; ++ii) {
synchronized( monitor) {
monitor.add(cnt);
}
synchronized(cnt) {
cnt++; // <-- why does moving the synchronized block here result in cnt being wrong?
}
}
好的,这个有点棘手。 cnt 是一个 Integer 对象并且 Java 不允许修改 Integer 对象(Integers 是不可变的),即使代码表明这就是这里发生的事情。但实际上会发生的是 cnt++ 将创建一个值为 cnt + 1 的新 Integer 并覆盖 cnt。 这就是代码的实际作用:
synchronized(cnt) {
Integer tmp = new Integer(cnt + 1);
cnt = tmp;
}
问题是当一个线程将创建一个新的 cnt 对象时,所有其他线程都在等待获取对旧对象的锁定。该线程现在释放旧的 cnt,然后将尝试获取新的 cnt 对象的锁并获取它,同时另一个线程获取旧的 cnt 对象的锁。突然有 2 个线程在临界区,执行相同的代码并导致竞争条件。这就是错误结果的来源。
如果您删除第一个同步块(带监视器的块),那么您的结果会变得更加错误,因为竞争的机会会增加。
一般来说,您应该尝试仅对 final 变量使用 synchronized 以防止这种情况发生。