[Java] マルチスレッドでjava.util.ArrayListオブジェクトが同期できていないことを示す例

環境

この記事の内容は、Xubuntu Linux 7.10, Java Standard Edition 1.5.0_13で確認しました。

以下のように、ひとつのjava.util.ArrayListオブジェクトに対して、複数のスレッドからオブジェクトをaddメソッドで追加するようにします。そうすると、すべてのスレッドが終了した後、ArrayListオブジェクトのsizeメソッドで得られる値が、期待したものとは異なることがあります。

import java.util.ArrayList;

class TestArrayList {

    public static void main(String[] arg) {
        new TestArrayList().test();
    }

    private void test() {
        // 複数のスレッドから同時アクセスされるオブジェクト
        ArrayList<Object> list = new ArrayList<Object>(1);

        // スレッドを実行する。
        int threads_count = 10;
        int add_count = 5000;
        Thread[] threads = new Thread[threads_count];
        for (int i = 0; i < threads.length; i++) {
            Thread thread = new Thread(new Appender(list, add_count));
            thread.start();
            threads[i] = thread;
        }

        // スレッドの終了を待つ。
        for (int i = 0; i < threads.length; i++) {
            try {
                threads[i].join();
            }
            catch (InterruptedException e) {
                System.out.println(e);
            }
        }

        // 結果を表示する。
        System.out.println("size=" + Integer.toString(list.size()) + ", expected=" + Integer.toString(threads_count * add_count));
    }

    /**
     * スレッドで実行されるRunnableクラス。
     */
    private class Appender implements Runnable {

        private ArrayList<Object> array;

        private int add_count;

        public Appender(ArrayList<Object> array, int add_count) {
            this.array = array;
            this.add_count = add_count;
        }

        public void run() {
            for (int i = 0; i < this.add_count; i++) {
                this.array.add(new Object());
            }
        }
    }
}

以下のスクリプトで期待通りの値にならない回数を数えたところ、おおよそ50%の確率でこの現象が発生しました。

yes | head -100 | while read foo
do
  java TestArrayList
done 2>/dev/null | perl -n -e 'if (m/^size=(.*), expected=(.*)$/) {
  if ($1 ne $2) {
    print;
  }
}' | wc -l

また、以下のような例外が発生することがありました。

Exception in thread "Thread-9" java.lang.ArrayIndexOutOfBoundsException
        at java.lang.System.arraycopy(Native Method)
        at java.util.ArrayList.ensureCapacity(ArrayList.java:170)
        at java.util.ArrayList.add(ArrayList.java:351)
        at TestArrayList$Appender.run(TestArrayList.java:47)
        at java.lang.Thread.run(Thread.java:595)

ArrayListオブジェクトの代わりに、メソッドがsynchronizedとなっているjava.util.Vectorオブジェクトを使うと、この現象は発生しませんでした。