1. 導入
現代のJava開発において、特にVirtual ThreadsやCompletableFutureを用いた非同期処理は非常に強力です。しかし、並行処理を無制限に行うと、外部APIのレート制限に抵触したり、データベースのコネクションプールを枯渇させたりするリスクがあります。今回解説する「Semaphore(セマフォ)」は、同時にアクセス可能なスレッド数を厳密に制御するための強力なツールです。本稿では、リソース保護の観点からSemaphoreの実践的な使い方を解説します。
2. 基礎知識
Semaphoreは、内部に「許可証(Permits)」のカウンターを持つ同期オブジェクトです。
・acquire(): 許可証を1つ取得します。許可証が0の場合は、誰かがリリースするまでスレッドは待機します。
・release(): 許可証を1つ返却します。これにより、待機中の他のスレッドが処理を再開できます。
これを利用することで、スループットを維持しつつ、システム全体の負荷を一定の範囲内に収めることが可能になります。
3. 実装/解決策
実務では、単に排他制御を行うだけでなく、try-finallyブロックと組み合わせて、例外発生時でも確実に許可証を解放する構成が必須です。また、ExecutorServiceやVirtual Threadsと組み合わせる際は、許可証の数をシステムリソースの限界に合わせて調整するのが定石です。
4. サンプルプログラム
以下は、同時に3つまでしか実行できない制限付きのタスク処理の実装例です。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
public class SemaphoreExample {
// 同時に実行可能なスレッド数を3に制限
private static final Semaphore semaphore = new Semaphore(3);
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++) {
int taskId = i;
executor.submit(() -> {
try {
// 許可証を取得(取得できるまで待機)
semaphore.acquire();
System.out.println("タスク " + taskId + " が実行中...");
// 擬似的な重い処理
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
// 処理終了後に必ず許可証を解放
System.out.println("タスク " + taskId + " 終了。枠を解放します。");
semaphore.release();
}
});
}
executor.shutdown();
}
}
5. 応用・注意点
現場で活用する際の重要なポイントをいくつか挙げます。
・tryAcquireの活用: 待機したくない(タイムアウトさせたい)場合は、tryAcquire(long timeout, TimeUnit unit)を使用してください。これにより、リソースが空かない場合に即座にフォールバック処理へ移行できます。
・公平性(Fairness): コンストラクタで new Semaphore(permits, true) と指定すると、先に待機していたスレッドから順に許可が与えられます(公平モード)。ただし、パフォーマンスにはわずかなオーバーヘッドが生じます。
・Virtual Threadsとの組み合わせ: Java 21以降のVirtual Threads環境では、OSスレッドをブロックしないため、Semaphoreによる制御がより軽量かつ効果的になります。ただし、許可証の数が大きすぎるとデータベース等のバックエンドがパンクするため、バックエンドの耐荷重に合わせて設定値を調整してください。
Semaphoreはシンプルなクラスですが、適切に使うことで予期せぬ障害を未然に防ぐ「守りの要」となります。ぜひ活用してください。

コメント