概要

業務システムでは注文番号やリクエストIDなど、複数スレッドから同時にアクセスされるカウンターが必要になる場面があります。単純な long++ は「読み取り・加算・書き込み」の3ステップで構成されており、複数スレッドが同時に実行すると番号の重複や欠番が発生します。synchronized で囲む方法もありますが、Java には CAS(Compare-And-Swap)命令を利用した AtomicLong や、高並行環境向けの LongAdder といった専用クラスが用意されています。この記事では、それぞれの仕組みと特性を整理したうえで、注文番号生成やアクセスカウンターといった実務パターンに応じた選び方を示します。外部ライブラリなしで動く完結したコードを通じて、競合状態を起こさない採番の設計を押さえます。

使いどころ

注文番号やリクエストIDなど、JVM 内で一意な連番をマルチスレッド環境から安全に払い出す

API ゲートウェイのアクセスカウンターを LongAdder で集計し、定期的に合計値をログ出力する

バッチ処理の進捗カウンターを AtomicLong で管理し、複数ワーカースレッドから処理済み件数を加算する

コード例

AtomicCounterDemo.java
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());
        }
    }
}

Java 8 / 17 / 21 の完全なサンプルコードは GitHub リポジトリ で確認できます。

Version Coverage

var による型推論、record での結果保持、switch 式による使い分け判定など、コードの簡潔さが向上する。AtomicLong のAPI自体に変更はない。

Java 17
// 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

標準 API(AtomicLong / LongAdder)単一 JVM 内の採番やカウント。依存なしで十分な性能が得られる。複数 JVM 間での一意性は保証されない。永続化も自前で行う必要がある。
Redis INCR複数サーバー間で共通の連番を払い出す必要がある場合。Redis への接続が必要。ネットワーク遅延が加算されるため、JVM 内で完結する場合には過剰。
DB シーケンス(SEQUENCE / 採番テーブル)永続化と複数 JVM 対応を同時に満たす必要がある場合。DB アクセスのオーバーヘッドがあるため、高頻度の採番にはバッチ取得の工夫が必要。

注意点

AtomicLong は単一 JVM 内でのみ有効。複数サーバーで共通の採番が必要な場合は DB 採番やRedis INCR 等の外部手段が必要になる

LongAdder の sum() は呼び出し時点の近似値を返す。他スレッドが加算中だと厳密な瞬間値にはならないため、連番の一意性が必要な用途には AtomicLong を使うこと

AtomicLong.compareAndSet を使ったリトライループは、競合が多い環境ではスピンが増えて CPU を消費する。高頻度なカウントには LongAdder を検討する

JVM 再起動でカウンターはリセットされる。永続化が必要なら初期値を DB やファイルから読み込む設計にする

AtomicLong のインスタンスを複数スレッドが共有するには、フィールドを static final にするかコンストラクタ注入で渡す。ローカル変数に作っても意味がない

FAQ

AtomicLong と LongAdder はどう使い分けるべきですか。

連番の一意性が必要なら AtomicLong、多スレッドからの高頻度な加算で最終合計が正確であれば良い場面では LongAdder を選んでください。LongAdder は内部でセルを分散させるため競合が少なくなります。

AtomicLong.get() は他スレッドの更新を即座に反映しますか。

はい。AtomicLong は volatile と CAS を使っているため、get() は最新の値を返します。ただし get() の直後に別スレッドが更新する可能性はあるため、get してから set する操作はアトミックではありません。

compareAndSet のリトライループはどの程度のスレッド数まで実用的ですか。

数十スレッド程度なら問題になりません。数百以上で常時競合するような環境ではスピン回数が増えるため、LongAdder や synchronized ブロックへの切り替えを検討してください。

関連書籍

この記事のテーマをさらに深く学びたい方へ。

※ Amazon アソシエイトリンクを含みます