【Java学習|初心者向け】JavaのCollections.synchronizedList()でマルチスレッドの落とし穴を回避!

皆さん、こんにちは!Javaエンジニアの〇〇です。
今回は、Javaで複数のスレッドが同時にリストにアクセスする際に発生しがちな問題と、その解決策である `Collections.synchronizedList()` について、初心者の方にも分かりやすく解説していきます。

なぜCollections.synchronizedList()が重要なのか?

Webアプリケーションやサーバーサイドのプログラムでは、複数の処理が同時に実行されることがよくあります。これを「マルチスレッド」と言います。
リスト(ArrayListなど)は、複数のスレッドから同時に参照・更新されると、予期せぬデータ破損やプログラムのクラッシュを引き起こす可能性があります。
例えば、あるスレッドがリストから要素を削除している最中に、別のスレッドがそのリストに要素を追加しようとすると、リストの内部状態が不正になり、後続の処理でエラーが発生する、といった具合です。
`Collections.synchronizedList()` は、このような「競合状態(Race Condition)」を防ぎ、リストをスレッドセーフにするための便利なラッパー(機能を追加する仕組み)です。

基礎知識:スレッドセーフとは?

「スレッドセーフ(Thread-safe)」とは、複数のスレッドから同時にアクセスされても、プログラムが正しく動作し続ける性質のことを指します。
Javaの標準的なコレクションクラス(ArrayList, HashMapなど)の多くは、デフォルトではスレッドセーフではありません。これは、パフォーマンスを優先するため、スレッド間の同期処理(後述)を意図的に行わないように設計されているからです。

スレッド間の同期処理とは、複数のスレッドが共有リソース(この場合はリスト)にアクセスする際に、一度に一つのスレッドしかアクセスできないように制御することです。これにより、データの整合性が保たれます。

Collections.synchronizedList()の実装方法

`Collections.synchronizedList()` を使うのは非常に簡単です。既存のListオブジェクトを引数として `Collections.synchronizedList()` メソッドに渡すだけで、スレッドセーフなListのラッパーが返されます。

例えば、ArrayListをスレッドセーフにしたい場合は、以下のように記述します。

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class SynchronizedListExample {

public static void main(String[] args) {
// 通常のArrayListを作成
List normalList = new ArrayList<>();
normalList.add(“Apple”);
normalList.add(“Banana”);

// Collections.synchronizedList()を使ってスレッドセーフなListを作成
List synchronizedList = Collections.synchronizedList(normalList);

// synchronizedListはスレッドセーフなので、複数のスレッドから安全にアクセスできます。
// 例えば、以下のようなコードで利用できます。(ここでは単に内容を表示します)
System.out.println(“Synchronized List: ” + synchronizedList);
}
}

この `synchronizedList` は、内部で各メソッド呼び出し時に同期処理を行ってくれます。

サンプルプログラム:スレッドセーフなリストの利用例

ここでは、複数のスレッドから `synchronizedList` に要素を追加する簡単な例を示します。

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class SynchronizedListUsage {

public static void main(String[] args) throws InterruptedException {
// スレッドセーフなListを作成
List synchronizedList = Collections.synchronizedList(new ArrayList<>());

// 複数のスレッドで実行するタスクを定義
Runnable task = () -> {
for (int i = 0; i < 100; i++) { // synchronizedListはスレッドセーフなので、add()操作は安全です。 synchronizedList.add(i); } }; // 10個のスレッドを作成し、タスクを実行 ExecutorService executor = Executors.newFixedThreadPool(10); for (int i = 0; i < 10; i++) { executor.submit(task); } // 全てのスレッドの終了を待つ executor.shutdown(); executor.awaitTermination(1, TimeUnit.MINUTES); // 最終的なリストのサイズを確認 (10スレッド 100要素 = 1000になるはず) System.out.println("Final list size: " + synchronizedList.size()); // リストの内容は順不同になる可能性がありますが、要素の欠落はありません。 // System.out.println("List content: " + synchronizedList); // 大量になるためコメントアウト } } このコードでは、10個のスレッドがそれぞれ100個の整数を `synchronizedList` に追加しています。`synchronizedList` を使っているおかげで、要素が欠落したり、リストが壊れたりすることなく、最終的なサイズが1000になります。

応用・注意点:Iteratorの利用とパフォーマンス

`Collections.synchronizedList()` は便利ですが、いくつか注意点があります。

Iteratorの利用時の注意

`synchronizedList` を `Iterator` で回す場合、`Iterator` 自体はスレッドセーフではありません。そのため、イテレーション中に他のスレッドがリストを変更すると、`ConcurrentModificationException` が発生する可能性があります。
これを避けるためには、`synchronizedList` の操作を `synchronized` ブロックで囲む必要があります。

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

public class SynchronizedListIterator {

public static void main(String[] args) {
List synchronizedList = Collections.synchronizedList(new ArrayList<>());
synchronizedList.add(“A”);
synchronizedList.add(“B”);
synchronizedList.add(“C”);

// イテレーション中に他のスレッドがリストを変更する可能性がある場合
// synchronizedブロックで囲むことが重要です。
synchronized (synchronizedList) {
Iterator iterator = synchronizedList.iterator();
while (iterator.hasNext()) {
String element = iterator.next();
System.out.println(element);
// ここでsynchronizedList.add(“D”);のような操作をするとConcurrentModificationExceptionが発生する
}
}
// synchronizedブロックの外でリストを変更するのは問題ありません(この例では行いませんが)
}
}

`synchronized (synchronizedList)` のように、`synchronizedList` オブジェクト自体をロック対象として指定することで、イテレーション中のリストの変更を防ぐことができます。

パフォーマンスへの影響

`Collections.synchronizedList()` は、各メソッド呼び出しでロックを取得・解放するため、シングルスレッドでの利用や、スレッドセーフが不要な場面では、パフォーマンスのオーバーヘッド(余計な処理による負担)が生じます。
もし、プログラム全体でスレッドセーフが必要であることが明確で、かつパフォーマンスが非常に重要な場合は、`java.util.concurrent` パッケージにある `CopyOnWriteArrayList` のような、より高度なスレッドセーフなコレクションクラスの利用を検討することもできます。 `CopyOnWriteArrayList` は、書き込み時にリストのコピーを作成するため、読み込み処理が多い場合に特に効果的です。

まとめ

`Collections.synchronizedList()` は、Javaでマルチスレッド環境においてリストを安全に扱うための強力なツールです。
特に、既存の `ArrayList` などのリストを簡単にスレッドセーフにしたい場合に非常に役立ちます。
ただし、Iteratorを使う際の注意点や、パフォーマンスへの影響も理解しておくと、より洗練されたコードを書くことができます。

ぜひ、皆さんの開発でも `Collections.synchronizedList()` を活用して、スレッドセーフなプログラムを作成してください!

コメント

タイトルとURLをコピーしました