スレッドの基本概念
プロセスとスレッドの違い
プロセスはOSがメモリ、ファイルハンドル、ネットワークソケットなどのリソースを割り当てる独立した実行単位である。一方、スレッドはプロセス内部で動作する軽量な実行単位で、同一プロセスのスレッドはヒープやメタスペースなどを共有する。
並行と並列の違い
- 並行(Concurrent):単一コアでタイムスライスを使って複数タスクを高速に切り替える。人間には同時に見えるが、実はシリアル実行。
- 並列(Parallel):マルチコアで複数タスクを物理的に同時実行。
スレッドの生成パターン
1. Threadクラスを継承
class CounterTask extends Thread {
private final int id;
CounterTask(int id) { this.id = ids; }
@Override
public void run() {
System.out.println("Task-" + id + " started");
}
public static void main(String[] args) {
new CounterTask(1).start();
new CounterTask(2).start();
}
}
2. Runnable実装
class LogWorker implements Runnable {
public void run() {
System.out.println("Logging from " + Thread.currentThread().getName());
}
public static void main(String[] args) {
Thread t1 = new Thread(new LogWorker(), "worker-A");
Thread t2 = new Thread(new LogWorker(), "worker-B");
t1.start();
}
}
3. Callable + FutureTask
class SumTask implements Callable<Integer> {
private final int n;
SumTask(int upper) { this.n = upper; }
public Integer call() {
return IntStream.rangeClosed(1, n).sum();
}
public static void main(String[] args) throws Exception {
FutureTask<Integer> ft = new FutureTask<>(new SumTask(100));
new Thread(ft).start();
System.out.println("Result = " + ft.get());
}
}
4. ExecutorServiceによる生成
class PoolDemo {
public static void main(String[] args) {
ExecutorService pool = Executors.newCachedThreadPool();
pool.execute(() -> System.out.println("Running in pool"));
pool.shutdown();
}
}
Runnable vs Callable
| 観点 | Runnable | Callable |
|---|---|---|
| 戻り値 | void | Generics指定可能 |
| 例外 | 内部で処理 | throws Exception可 |
| 組み合わせ | Thread直接 | FutureTask経由 |
Thread#start と直接の run 呼び出し
start():新規スタックフレームを生成しOSスケジューラに登録。run():現在のスレッドで普通のメソッド呼び出し。
ライフサイクルステート
NEW → start() → RUNNABLE → CPU取得 → RUNNING → 完了 → TERMINATED
↓
BLOCKED, WAITING, TIMED_WAITING
join で順序制御
public class OrderedThreads {
public static void main(String[] args) throws InterruptedException {
Thread first = new Thread(() -> System.out.println("Alpha"));
Thread second = new Thread(() -> {
try { first.join(); } catch (InterruptedException ignore) {}
System.out.println("Beta");
});
Thread third = new Thread(() -> {
try { second.join(); } catch (InterruptedException ignore) {}
System.out.println("Gamma");
});
third.start(); second.start(); first.start();
}
}
wait/notify モデル
class Signal {
private static final Object lock = new Object();
private static boolean ready = false;
public static void main(String[] args) {
Thread waiter = new Thread(() -> {
synchronized (lock) {
while (!ready) {
try { lock.wait(); } catch (InterruptedException ignored) {}
}
System.out.println("Received signal");
}
});
Thread notifier = new Thread(() -> {
synchronized (lock) {
ready = true;
lock.notifyAll();
}
});
waiter.start();
notifier.start();
}
}
wait と sleep の違い
- 所属:wait は Object のインスタンスメソッド、sleep は Thread の static メソッド。
- ロック:wait はモニタロックを解放、sleep は保持したまま。
- 起床条件:wait は notify/notifyAll またはタイムアウト、sleep は必ず時間経過。
スレッドセーフティ基礎
競合状態(Race Condition)
複数スレッドが共有変数に同時にアクセスし、結果が最終的な実行順序に依存する問題。
メモリ可視性(Visibility)
CPUキャッシュやコンパイラの最適化により、あるスレッドが書き込んだ値が他スレッドに即座に見えない問題。
同期プリミティブ
synchronized:モニタロックによる相互排除volatile:可視性の保証と先行発生関係java.util.concurrent.locks:より柔軟なロック機構Atomic*クラス:CAS(Compare-And-Swap)によるロックフリー更新
Executorフレームワーク
class FixedPoolExample {
public static void main(String[] args) {
ExecutorService exec = Executors.newFixedThreadPool(4);
IntStream.range(0, 10)
.forEach(i -> exec.submit(() -> System.out.println("Task " + i)));
exec.shutdown();
}
}
カスタムThreadFactory
ThreadFactory factory = r -> {
Thread t = new Thread(r);
t.setDaemon(false);
t.setName("biz-thread-" + System.nanoTime());
return t;
};
ExecutorService es = Executors.newFixedThreadPool(4, factory);
まとめ
Java の並行 API はスレッド生成から高度な Executor フレームワークまで幅広く揃っており、適切な同期戦略を選ぶことでスケーラブルで安全なアプリケーションを構築できる。