5

以下は簡単な Java プログラムです。「cnt」と呼ばれるカウンターがあり、インクリメントされてから「monitor」と呼ばれるリストに追加されます。「cnt」は​​複数のスレッドでインクリメントされ、値は複数のスレッドで「monitor」に追加されます。

メソッド「go()」の最後で、cnt と monitor.size() は同じ値になるはずですが、そうではありません。monitor.size() には正しい値があります。

コメント化された同期ブロックの 1 つをコメント解除し、現在コメント化されていないブロックをコメント化してコードを変更すると、コードは期待どおりの結果を生成します。また、スレッド カウント (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?
    }
}

まず、モニター オブジェクトがスレッド間で共有されるため、そのロックを取得すると (つまり、同期が行われます)、ブロック内のコードが一度に 1 つのスレッドによってのみ実行されるようになります。したがって、外側の 1 つの内部で 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 オブジェクトを変更することを許可していません (Integer は不変です)。コードは、これがここで起こっていることであることを示しています。しかし、実際に起こることは、cnt++ が値 cnt + 1 で新しい整数を作成し、cnt をオーバーライドすることです。これはコードが実際に行うことです:

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

問題は、あるスレッドが新しい cnt オブジェクトを作成している間に、他のすべてのスレッドが古いオブジェクトのロックを取得するのを待っていることです。スレッドは古い cnt を解放し、別のスレッドが古い cnt オブジェクトのロックを取得している間に、新しい cnt オブジェクトのロックを取得しようとします。突然 2 つのスレッドがクリティカル セクションに入り、同じコードを実行して競合状態を引き起こします。ここから間違った結果が生まれます。

最初の同期ブロック (モニターのあるブロック) を削除すると、競合の可能性が高まるため、結果はさらに間違ったものになります。

一般に、このようなことが起こらないように、final 変数のみに synchronized を使用するようにしてください。

于 2016-02-05T02:59:40.980 に答える