Java はもともと高並行性のために特化して設計されたわけではありませんが、強力な並行プログラミングのサポートを提供しており、マルチスレッド処理を効率的に行うことができます。以下に、Java の並行処理について詳しく説明します。
1. Java の並行処理モデル
1.1 スレッド(Thread)
Java は java.lang.Thread クラスを提供し、デフォルトでプログラムは 1 つのメインスレッドで実行されます。開発者は Thread を作成することで並行処理を実装できます。
class MyThread extends Thread { public void run() { System.out.println("スレッドが実行中..."); } } public class Main { public static void main(String[] args) { MyThread thread = new MyThread(); thread.start(); // スレッドを開始 } }
特徴: - Java は 共有メモリベースの並行処理モデル を採用しており、複数のスレッドが同じメモリデータにアクセス可能。 - スレッドの切り替えは JVM と OS のスケジューラ によって管理される。
1.2 Runnable と Callable
Java では Thread クラスのほかに、Runnable や Callable インターフェースを使うことで、より柔軟なスレッドの実装が可能。
class MyTask implements Runnable { public void run() { System.out.println("Runnable スレッド実行中..."); } } class MyCallable implements Callable<Integer> { public Integer call() { return 42; } }
Runnable には戻り値がないが、Callable は計算結果を返すことができ、例外もスロー可能。
2. Java の並行処理の主要なサポート機能
2.1 synchronized キーワード
Java は synchronized キーワードを提供し、スレッドの安全性 を確保できます。複数のスレッドが同時に共有リソースへアクセスしないようにします。
class Counter { private int count = 0; public synchronized void increment() { count++; } public synchronized int getCount() { return count; } }
特徴:
- synchronized メソッドやブロックは オブジェクトロック を取得し、1 つのスレッドのみが実行可能。
- JVM による最適化(バイアスロック、軽量ロック、重量ロック)により、JDK 1.6 以降 synchronized の性能が向上。
2.2 volatile キーワード
volatile キーワードは 可視性 と 順序保証 を提供するが、原子性は保証しない。
class VolatileExample { private volatile boolean flag = true; public void stop() { flag = false; } }
特徴:
- volatile 変数の変更がすべてのスレッドに即時反映されるため、キャッシュによるデータ不整合を防止。
- 状態フラグ には適しているが、カウンターの増減 などの操作には向かない。
2.3 java.util.concurrent パッケージ
synchronized や volatile の欠点を補うため、JDK 1.5 で java.util.concurrent パッケージが導入され、高効率な並行プログラミングが可能になった。
(1) ReentrantLock
synchronized よりも柔軟なロック制御を可能にする ReentrantLock。
class Counter { private final ReentrantLock lock = new ReentrantLock(); private int count = 0; public void increment() { lock.lock(); try { count++; } finally { lock.unlock(); } } }
(2) アトミック変数(AtomicInteger, AtomicLong など)
AtomicInteger は ロックなしでスレッド安全 なカウンターを提供し、CAS(Compare-And-Swap) を使用。
AtomicInteger count = new AtomicInteger(0); count.incrementAndGet();
(3) スレッドプール(ThreadPoolExecutor)
スレッドプールはスレッドの作成・破棄のオーバーヘッドを削減し、パフォーマンスを向上。
ExecutorService executor = Executors.newFixedThreadPool(5); executor.execute(() -> System.out.println("タスク実行中")); executor.shutdown();
(4) 並行コレクション
ConcurrentHashMap:スレッド安全な高性能 Map。CopyOnWriteArrayList:書き込み時にコピーを作成するため、読み取りが多く書き込みが少ない シナリオに適している。
3. Java の並行処理の基盤
Java の並行処理は、JVM と OS のサポートに依存している。
3.1 Java メモリモデル(JMM)
Java では Java Memory Model (JMM) により、スレッドがデータをどのように共有するかを定義。
- メインメモリ(Main Memory):すべての変数が保存され、すべてのスレッドからアクセス可能。
- 作業メモリ(Working Memory):各スレッドが独自にキャッシュする領域。
3.2 命令の再順序化
CPU や JVM の最適化によって 命令の再順序化 が行われるが、volatile キーワードを使用するとこれを防止でき、コード実行の順序を保証可能。
4. Java の高並行環境での応用
Java の並行処理機能は以下の分野で活用されている: 1. 高並行 Web サーバー(Tomcat、Jetty、Netty)。 2. ビッグデータ処理(Hadoop、Spark)。 3. データベースアクセスの並列化(接続プール HikariCP)。 4. 金融システム(低遅延の取引処理)。
5. Java 並行処理の課題
Java は強力な並行処理サポートを持つが、いくつかの課題もある: 1. スレッドの切り替えコストが高い:Go 言語の Goroutine と比較すると、Java のスレッドはコンテキストスイッチのオーバーヘッドが大きい。 2. 同期コードの複雑性:デッドロックや競合状態を適切に管理する必要がある。 3. GC(ガベージコレクション)の影響:高並行環境では GC による一時停止が発生する可能性がある。
6. 結論
Java は「並行処理のために生まれた」言語ではないが、強力な並行処理機能を提供し、高並行システムの開発に適している。
synchronized から java.util.concurrent まで、さらには Java 8 の CompletableFuture など、Java の並行処理は進化し続けており、多くのシステムで重要な選択肢となっている。