java-recipes

ホーム 並行処理・メモリ › P-01

P-01: スレッドセーフな採番(AtomicLong / LongAdder)

複数スレッドから同時に番号を採番すると、通常の long++ では重複が発生します。AtomicLong は CAS(Compare-And-Swap)命令で synchronized なしに ATOMIC 操作を実現します。 注文番号・リクエストIDなど、業務システムで必須のパターンです。

AtomicLong vs LongAdder の使い分け

クラス特徴向いている用途
AtomicLongCAS 命令で厳密な連番を保証注文番号・リクエストID・シーケンス番号
LongAdder内部セル分散で高並行時に高速アクセスカウンター・統計集計
synchronizedロックで排他制御、低オーバーヘッド時有効複合操作(複数フィールドの同時更新)

サンプルコード

Sample.java
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder;

public class AtomicCounterSample {

    // ---- AtomicLong: CAS 命令による ATOMIC インクリメント ----
    //
    // 通常の long++ は「読み取り→加算→書き込み」の3ステップ。
    // 複数スレッドが同時実行すると番号が重複する(Race Condition)。
    // AtomicLong は 1 命令で ATOMIC に操作するため synchronized 不要。

    static final AtomicLong atomicCounter = new AtomicLong(0);

    public static long atomicIncrement() {
        return atomicCounter.incrementAndGet(); // +1 して新しい値を返す
    }

    // ---- 注文番号生成のシングルトンパターン ----

    static class OrderNumberGenerator {
        // JVM 起動時に 1 つだけ生成。static final で確実にシングルトン。
        private static final AtomicLong sequence = new AtomicLong(10000);

        /** 「ORD-10001」形式の注文番号を生成する(スレッドセーフ) */
        public static String next() {
            return "ORD-" + sequence.incrementAndGet();
        }

        public static long current() {
            return sequence.get();
        }
    }

    // ---- LongAdder: 高並行環境での集計(Java 8+)----
    //
    // 内部セルに分散して加算するため、多数スレッドが同時加算する場面で
    // AtomicLong より高速。
    // ただし sum() 取得の瞬間は厳密な連番ではないため、
    // 一意性が必要な採番には使わないこと。

    static final LongAdder adderCounter = new LongAdder();

    public static void main(String[] args) throws InterruptedException {
        int threads = 100;
        int incrementsPerThread = 1000;
        long expected = (long) threads * incrementsPerThread;

        // AtomicLong で 100 スレッド × 1000 回のインクリメント
        AtomicLong counter = new AtomicLong(0);
        ExecutorService pool = Executors.newFixedThreadPool(threads);
        CountDownLatch latch = new CountDownLatch(threads);

        for (int i = 0; i < threads; i++) {
            pool.submit(() -> {
                for (int 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 ? " ✓" : " ✗"));

        // 注文番号生成デモ
        System.out.println("\n--- 注文番号生成 ---");
        for (int i = 0; i < 5; i++) {
            System.out.println(OrderNumberGenerator.next());
        }

        // LongAdder デモ
        LongAdder adder = new LongAdder();
        ExecutorService pool2 = Executors.newFixedThreadPool(threads);
        CountDownLatch latch2 = new CountDownLatch(threads);
        for (int i = 0; i < threads; i++) {
            pool2.submit(() -> {
                for (int j = 0; j < incrementsPerThread; j++) {
                    adder.increment();
                }
                latch2.countDown();
            });
        }
        latch2.await();
        pool2.shutdown();
        System.out.println("\nLongAdder合計: " + adder.sum()
            + (adder.sum() == expected ? " ✓" : " ✗"));
    }
}

よくあるミス・注意点

⚠️ long++ はスレッドアンセーフ

counter++ は「読み取り・加算・書き込み」の3命令です。 スレッド A が読み取り中にスレッド B が同じ値を読むと、同じ番号が2回採番されます。 必ず AtomicLong.incrementAndGet() を使ってください。

⚠️ LongAdder は一意の連番には使えない

LongAddersum() は内部セルの合計値を返しますが、 取得タイミングによって「最終的な正確な合計値」と「瞬間的な近似値」が異なります。 注文番号のように一意性・連番性が必要な用途には AtomicLong を使ってください。

⚠️ AtomicLong は単一 JVM 内でのみ有効

複数サーバ(マルチ JVM)が同じ採番を共有する場合、AtomicLong では サーバをまたいだ一意性は保証できません。その場合は P-02(DB 採番)を使ってください。

テストする観点

  • ✅ 100スレッド × 1000回インクリメントした結果が 100,000 と一致するか
  • incrementAndGet() の戻り値に重複がないか(採番値セットの確認)
  • ✅ シングルトンの OrderNumberGenerator.next() が並列実行時も一意か
  • LongAddersum() が全加算後に期待値と一致するか
  • ✅ 初期値(new AtomicLong(10000))から始まる番号列が正しいか

GitHub でソースコードを見る →