5

下面是一个简单的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");
    }    
}
4

1 回答 1

3

好的,让我们看看您提到的不同可能性:

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?
    }
}

首先,监视器对象在线程之间共享,因此对其进行锁定(这就是 synchronized 所做的)将确保块内的代码一次只能由一个线程执行。因此,外部的 2 个同步内部不是必需的,无论如何代码都受到保护。

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 对象(整数是不可变的),即使代码表明这就是这里发生的事情。但实际上会发生的是,cnt++ 将创建一个值为 cnt + 1 的新 Integer 并覆盖 cnt。这就是代码的实际作用:

synchronized(cnt) {
  Integer tmp = new Integer(cnt + 1);
  cnt = tmp;
}

问题是一个线程将创建一个新的 cnt 对象,而所有其他线程都在等待锁定旧对象。该线程现在释放旧的 cnt,然后将尝试在新的 cnt 对象上获取锁,并在另一个线程在旧的 cnt 对象上获取锁时获取它。突然有 2 个线程处于临界区,执行相同的代码并导致竞争条件。这就是错误结果的来源。

如果您删除第一个同步块(带有监视器的块),那么您的结果会变得更加错误,因为比赛的机会增加了。

一般来说,您应该尝试仅在最终变量上使用同步来防止这种情况发生。

于 2016-02-05T02:59:40.980 回答