Set操作、特に変更操作と拡張for構文を使ったiterationを別スレッドで処理する時のメモ。
int値フィールド一つだけの簡単なSomeObjectをSetにaddするWriteThreadと、Setから拡張for構文でiterationし、int値の合計を表示するEnumThreadの二つを起動する。
SetTest1.java:
import java.util.*; import java.util.concurrent.*; public class SetTest1 { final Set<SomeObject> objects = Collections.synchronizedSet(new HashSet<SomeObject>()); Random rnd = new Random(); public static void main(String args[]) { new SetTest1().test(); } public void test() { new EnumThread().start(); new WriteThread().start(); } class SomeObject { int val; public SomeObject(int val_) { val = val_; } public int getVal() { return val; } } class EnumThread extends Thread { public void run() { while (true) { long sum = 0; for (SomeObject o : objects) { sum += o.getVal(); } System.out.println("sum = " + sum); try { sleep(100); } catch (InterruptedException e) { } } } } class WriteThread extends Thread { public void run() { while (true) { objects.add(new SomeObject(rnd.nextInt(10))); try { sleep(10); } catch (InterruptedException e) { } } } } }
Collections.synchronizedSet()はSetに対する操作を同期化してくれるが、iteration経由での操作については対象外。
実行してみると、かなり早いタイミングで java.util.ConcurrentModificationException が発生してしまう。
> javac SetTest1.java > java SetTest1 sum = 0 sum = 48 sum = 78 sum = 128 sum = 175 sum = 217 Exception in thread "Thread-0" java.util.ConcurrentModificationException at java.util.HashMap$HashIterator.nextEntry(HashMap.java:793) at java.util.HashMap$KeyIterator.next(HashMap.java:828) at SetTest1$EnumThread.run(SetTest1.java:35)
java.util.Collections.synchronizedSet()のJavaDocにも記載されているが、iteratorを使う時は手動でコレクションに対するsynchronized()で囲ってみる。
SetTest2.java:
// ... class EnumThread extends Thread { public void run() { while (true) { long sum = 0; synchronized(objects) { for (SomeObject o : objects) { sum += o.getVal(); } } System.out.println("sum = " + sum); try { sleep(100); } catch (InterruptedException e) { } } } } // ...
これであれば、ConcurrentModificationException 例外は発生しない。
複数スレッドからの操作で、全体として変更操作よりもiteration操作の方が明らかに多い場合は java.util.concurrent.CopyOnWriteArraySet<E> を使ってみる。iteration中の別スレッドからの変更(追加/削除)は、配列をコピーして行われる。変更操作はiteratorに反映されないため、iteratorは"スナップショット"として使うことになる。iteratorに対する要素変更はサポートされず、実行するとUnsupportedOperationExceptionがthrowされる。
//... (SetTest1.javaと同じ) public class SetTest3 { final Set<SomeObject> objects = new CopyOnWriteArraySet<SomeObject>(); Random rnd = new Random(); public static void main(String args[]) { new SetTest3().test(); } //... (SetTest1.javaと同じ)
手動でsynchronized()で囲うよりは、CopyOnWriteの方が個人的には好み。
コメント