概要
業務システムでは注文番号やリクエストIDなど、複数スレッドから同時にアクセスされるカウンターが必要になる場面があります。単純な long++ は「読み取り・加算・書き込み」の3ステップで構成されており、複数スレッドが同時に実行すると番号の重複や欠番が発生します。synchronized で囲む方法もありますが、Java には CAS(Compare-And-Swap)命令を利用した AtomicLong や、高並行環境向けの LongAdder といった専用クラスが用意されています。この記事では、それぞれの仕組みと特性を整理したうえで、注文番号生成やアクセスカウンターといった実務パターンに応じた選び方を示します。外部ライブラリなしで動く完結したコードを通じて、競合状態を起こさない採番の設計を押さえます。
使いどころ
注文番号やリクエストIDなど、JVM 内で一意な連番をマルチスレッド環境から安全に払い出す
API ゲートウェイのアクセスカウンターを LongAdder で集計し、定期的に合計値をログ出力する
バッチ処理の進捗カウンターを AtomicLong で管理し、複数ワーカースレッドから処理済み件数を加算する
コード例
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder;
public class AtomicCounterDemo {
/** 注文番号を AtomicLong で安全に生成するシングルトン */
static class OrderNumberGenerator {
private static final AtomicLong sequence = new AtomicLong(10000);
public static String next() {
return "ORD-" + sequence.incrementAndGet();
}
public static long current() {
return sequence.get();
}
}
public static void main(String[] args) throws InterruptedException {
int threads = 100;
int incrementsPerThread = 1000;
long expected = (long) threads * incrementsPerThread;
var counter = new AtomicLong(0);
var pool = Executors.newFixedThreadPool(threads);
var latch = new CountDownLatch(threads);
for (var i = 0; i < threads; i++) {
pool.submit(() -> {
for (var j = 0; j < incrementsPerThread; j++) {
counter.incrementAndGet();
}
latch.countDown();
});
}
latch.await();
pool.shutdown();
System.out.println("期待値: " + expected);
System.out.println("AtomicLong 結果: " + counter.get()
+ (counter.get() == expected ? " -> 正確" : " -> 欠損あり"));
var adder = new LongAdder();
var pool2 = Executors.newFixedThreadPool(threads);
var latch2 = new CountDownLatch(threads);
for (var i = 0; i < threads; i++) {
pool2.submit(() -> {
for (var j = 0; j < incrementsPerThread; j++) {
adder.increment();
}
latch2.countDown();
});
}
latch2.await();
pool2.shutdown();
System.out.println("LongAdder 結果: " + adder.sum()
+ (adder.sum() == expected ? " -> 正確" : " -> 欠損あり"));
System.out.println("\n--- 注文番号生成 ---");
for (var i = 0; i < 5; i++) {
System.out.println(OrderNumberGenerator.next());
}
}
}Version Coverage
var による型推論、record での結果保持、switch 式による使い分け判定など、コードの簡潔さが向上する。AtomicLong のAPI自体に変更はない。
// Java 17: var + record で簡潔に
var counter = new AtomicLong(0);
var pool = Executors.newFixedThreadPool(100);
record CounterResult(long value, String type) {}
// switch 式で用途に応じた推奨を返す
String rec = switch (useCase) {
case ORDER_NUMBER -> "AtomicLong";
case ACCESS_COUNT -> "LongAdder";
};Library Comparison
注意点
AtomicLong は単一 JVM 内でのみ有効。複数サーバーで共通の採番が必要な場合は DB 採番やRedis INCR 等の外部手段が必要になる
LongAdder の sum() は呼び出し時点の近似値を返す。他スレッドが加算中だと厳密な瞬間値にはならないため、連番の一意性が必要な用途には AtomicLong を使うこと
AtomicLong.compareAndSet を使ったリトライループは、競合が多い環境ではスピンが増えて CPU を消費する。高頻度なカウントには LongAdder を検討する
JVM 再起動でカウンターはリセットされる。永続化が必要なら初期値を DB やファイルから読み込む設計にする
AtomicLong のインスタンスを複数スレッドが共有するには、フィールドを static final にするかコンストラクタ注入で渡す。ローカル変数に作っても意味がない
FAQ
連番の一意性が必要なら AtomicLong、多スレッドからの高頻度な加算で最終合計が正確であれば良い場面では LongAdder を選んでください。LongAdder は内部でセルを分散させるため競合が少なくなります。
はい。AtomicLong は volatile と CAS を使っているため、get() は最新の値を返します。ただし get() の直後に別スレッドが更新する可能性はあるため、get してから set する操作はアトミックではありません。
数十スレッド程度なら問題になりません。数百以上で常時競合するような環境ではスピン回数が増えるため、LongAdder や synchronized ブロックへの切り替えを検討してください。