本ページはプロモーションが含まれています

Java入門

Javaのスレッドセーフとは?実装方法・確認方法・よくある落とし穴を解説

トム

・都内自社開発企業勤務/Javaバックエンドエンジニア
/Java歴10年以上 ・首都圏在住30代
・資格:基本情報技術者/応用情報技術者/Java Silver/Python3エンジニア認定基礎 詳細なプロフィール

マルチスレッドのJavaコードを書いていて「スレッドセーフって何?どう実装すればいい?」と悩んでいませんか?

結論から言うと、スレッドセーフとは複数スレッドから同時にアクセスされても正しく動作する性質です。実現方法は synchronizedvolatilejava.util.concurrent・不変オブジェクト・ThreadLocal の5つが主軸です。

この記事では、以下のことがわかります。

  • スレッドセーフとは何か(定義・問題事例)
  • Javaでスレッドセーフを実現する主な実装方法(synchronizedvolatileAtomicIntegerConcurrentHashMap
  • シングルトンパターンのスレッドセーフな実装方法
  • 実践で踏みやすい落とし穴と対策
  • スレッドセーフの確認・チェック方法

初心者向けに具体的なコード例を交えて解説します。

スレッドセーフとは何か?

まず、「スレッドセーフ」とは何かを理解するために、Javaにおけるスレッドとその重要性から見ていきましょう。

Javaにおけるスレッド(thread)とは?

プログラムの実行単位を「プロセス」と呼びます。一つのプロセスの中で、さらに枝分かれして同時に実行される処理の流れが「スレッド」です。

一つのプログラムで複数のスレッドを動かすことを「マルチスレッド」と呼びます。マルチスレッドを利用すると、例えば以下のようなメリットがあります。

メリット

  • 応答性の向上: 時間のかかる処理を別のスレッドに任せることで、ユーザーインターフェースが固まらず、操作を受け付け続けられる。
  • 処理能力の向上: マルチコアCPUの場合、複数のスレッドを別々のコアで同時に実行することで、全体の処理時間を短縮できる。
  • リソースの有効活用: ネットワークからの応答待ちなど、処理が一時的に停止する時間に、他のスレッドを実行することで、CPUを有効に活用できる。

このように、マルチスレッドはJavaアプリケーションのパフォーマンスや使いやすさを向上させるために欠かせない技術です。

スレッドセーフの定義

スレッドセーフとは、複数のスレッドから同時にアクセスされても、プログラムが意図した通りに正しく動作することを指します。

あるクラスやメソッドがスレッドセーフとは、複数のスレッドから同時に利用しても、データの不整合や予期せぬエラーが発生しない状態を指します。特別な制御なしにインスタンスを安全に共有できることが条件です。

スレッドセーフとメモリ領域(ヒープ・スタック)の関係

「なぜ複数スレッドでデータの問題が起きるのか」を理解するには、Javaのメモリ領域を知ることが重要です。

Javaには主に2つのメモリ領域があります。

メモリ領域格納されるものスレッド間の共有スレッドセーフ性
スタック領域ローカル変数、メソッド呼び出し情報各スレッドで独立安全(共有されない)
ヒープ領域インスタンス変数、クラス変数(static)全スレッドで共有危険(同時アクセス可能)

ローカル変数はスタック領域(スレッド固有)に置かれるためスレッドセーフです。一方、インスタンス変数やクラス変数はヒープ領域に置かれ全スレッドから参照できるため、適切な制御がないとデータ破壊につながります。

public class Counter {
    private int count = 0; // ヒープ領域(インスタンス変数)→全スレッドで共有→危険

    public void increment() {
        int tmp = count; // スタック領域(ローカル変数)→スレッド固有→安全
        tmp++;
        count = tmp;     // ここで複数スレッドが同時に書き込むとデータが壊れる
    }
}

スレッドセーフでないと何が起きる?

もしプログラムがスレッドセーフでない場合、複数のスレッドが同時にデータにアクセスすると問題が発生します。これを「競合状態(Race Condition)」と呼びます。

代表的な問題は以下のとおりです。

データの不整合

あるスレッドがデータを更新している途中で、別のスレッドがそのデータを読み込んだり、さらに更新したりすると、データが中途半端な状態になったり、意図しない値になったりします。

: 銀行口座の残高を更新する処理で、あるスレッドが入金処理をしている最中に、別のスレッドが出金処理をすると、最終的な残高が合わなくなる。

予期せぬ例外

データが不正な状態になることで、NullPointerExceptionArrayIndexOutOfBoundsExceptionなどの予期せぬ例外が発生します。

デッドロック

複数のスレッドが互いに相手のリソース(データやロック)を待ってしまい、処理が全く進まなくなる状態です。こうなると、アプリケーションが停止してしまうこともあります。

ライブロックとスターベーション

デッドロック以外にも、マルチスレッド特有の問題があります。

  • ライブロック(Livelock): 2つのスレッドが互いに相手に「どうぞ」と譲り合いを繰り返し、どちらも実際の処理が進まない状態。デッドロックと異なり、スレッドは動き続けているが何も進まない。
  • スターベーション(Starvation): 優先度の低いスレッドが、優先度の高いスレッドに常にCPU時間を奪われ、永遠に実行機会を得られない状態。

Javaでスレッドセーフを実現する方法

スレッドセーフを実現するには主に5つのアプローチがあります。用途に応じた使い分けがパフォーマンスの鍵です。

手法スレッドセーフアトミック性パフォーマンス推奨ユースケース
synchronized低(待機発生)シンプルな排他制御
volatile○(可視性のみ)フラグ変数の読み書き
AtomicInteger高(CAS使用)カウンター・数値更新
ReentrantLock中〜高タイムアウト付きロック
ConcurrentHashMap○(一部操作)高(分割ロック)高並列な読み書き

同期化(synchronized)の基本

最も基本的なスレッドセーフ実現方法は「同期化(Synchronization)」です。Javaではsynchronizedキーワードを使って実現します。

synchronizedは、メソッドや特定のコードブロックに対して、一度に1つのスレッドだけが実行できます。これを「排他制御」や「ロック」と呼びます。

ただし、synchronizedを多用するとスレッドがロックを待つ時間が増え、パフォーマンスが低下します。多用は避けましょう。

synchronizedメソッド

public synchronized void increment() {
    count++; // この操作は一度に一つのスレッドしか実行できない
}

メソッド全体を同期化します。このメソッドが実行されている間、他のスレッドは同じインスタンスのsynchronizedメソッドを実行できません。

synchronizedブロック

public void increment() {
    // ... 他の処理 ...
    synchronized (this) {
        count++; // このブロックだけを同期化
    }
    // ... 他の処理 ...
}

コードの一部だけを同期化します。同期化の範囲を最小限にすることで、パフォーマンスへの影響を抑えられます。synchronizedの後ろの(this)は、ロックを取得する対象(モニターオブジェクト)を指定します。

synchronized なし vs あり:実行結果の比較

実際にsynchronizedの有無でどれほど結果が変わるか確認してみましょう。

// 非スレッドセーフなカウンター(synchronizedなし)
public class UnsafeCounter {
    private int count = 0;
    public void increment() { count++; }
    public int getCount() { return count; }
}

// スレッドセーフなカウンター(synchronizedあり)
public class SafeCounter {
    private int count = 0;
    public synchronized void increment() { count++; }
    public int getCount() { return count; }
}

// 1000スレッドで各1000回インクリメント → 期待値は 1,000,000
// UnsafeCounter の結果例: 987,342(毎回変わる不正な値)
// SafeCounter  の結果例: 1,000,000(常に正しい値)

volatile修飾子の役割

volatile修飾子は、主に「メモリの可視性」を保証するために使われます。

マルチスレッド環境では、各スレッドが変数をCPUキャッシュに保持します。そのため、あるスレッドが変数の値を変更しても、他のスレッドからは見えません。

volatileを付けた変数は、常にメインメモリから読み書きされます。volatileを付けることで、あるスレッドによる変更が、他のすべてのスレッドから必ず見えるようになります。

private volatile boolean running = true;

public void stop() {
    running = false; // この変更は他のスレッドからすぐに見える
}

public void run() {
    while (running) {
        // ... 処理 ...
    }
}

volatileは、変数の変更を他のスレッドに即座に知らせることで、特定の状況でのスレッドセーフに貢献します。

ただし、volatileはアトミック性(複数の操作を一体として実行すること)は保証しません。例えば、count++のような「読み込み→変更→書き込み」という一連の操作は、volatileだけではスレッドセーフになりません。この場合はsynchronizedや後述するAtomicクラスが必要です。

java.util.concurrent(Atomic・ConcurrentHashMap)でスレッドセーフを高速化

Java 5で導入され、現在のJava(21/25)ではさらに拡張されているjava.util.concurrentパッケージには、スレッドセーフなコレクションや、より柔軟なロック機構、アトミックな操作を行うクラスなどが含まれます。

Atomicクラス

AtomicInteger, AtomicLong, AtomicReferenceなどがあります。これらは、count++のような複合操作をアトミックに実行できます。synchronizedより軽量で高速です。

private AtomicInteger count = new AtomicInteger(0);
public void increment() {
    count.incrementAndGet(); // アトミックにインクリメント
}

Lockインターフェース(ReentrantLock)

ReentrantLockなどがあります。synchronizedよりも高度なロック機能を提供します。例えば、ロックの取得を試みて、取得できなかったら待たずに別の処理を行う、といった制御が可能です。

import java.util.concurrent.locks.ReentrantLock;

public class SafeCounter {
    private final ReentrantLock lock = new ReentrantLock();
    private int count = 0;

    public void increment() {
        lock.lock(); // ロック取得
        try {
            count++;
        } finally {
            lock.unlock(); // 必ずfinallyで解放
        }
    }

    // タイムアウト付きでロックを試みる
    public boolean tryIncrement() throws InterruptedException {
        if (lock.tryLock()) { // ロックを取得できた場合のみ実行
            try {
                count++;
                return true;
            } finally {
                lock.unlock();
            }
        }
        return false; // ロック取得できなかった場合
    }
}

Concurrentコレクション

ConcurrentHashMap, CopyOnWriteArrayListなどがあります。

ConcurrentHashMapCopyOnWriteArrayListは、synchronizedでラップされたコレクションよりも高いパフォーマンスを発揮するスレッドセーフなコレクションです。

シングルトンパターンのスレッドセーフな実装

マルチスレッド環境で特に注意が必要なのが「シングルトンパターン」です。シングルトンはインスタンスを1つだけ生成する設計ですが、スレッドセーフに実装しないと複数のインスタンスが生成される危険があります。

NG例: スレッドセーフでないシングルトン

public class Singleton {
    private static Singleton instance;

    public static Singleton getInstance() {
        if (instance == null) {           // ①複数スレッドが同時にnullチェック
            instance = new Singleton();   // ②複数インスタンスが生成される可能性
        }
        return instance;
    }
}

①のnullチェックと②のインスタンス生成の間に別のスレッドが割り込むと、複数のインスタンスが生成されてしまいます。

OK例1: synchronizedで解決(シンプル・低パフォーマンス)

public class Singleton {
    private static Singleton instance;

    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

シンプルで確実ですが、getInstance()が呼ばれるたびにロックを取得するためパフォーマンスが低下します。

OK例2: ダブルチェックロッキング(パフォーマンスと安全性を両立)

public class Singleton {
    // volatileが必須(Java5以降)
    private static volatile Singleton instance;

    public static Singleton getInstance() {
        if (instance == null) {              // 1回目のチェック(ロックなし)
            synchronized (Singleton.class) {
                if (instance == null) {      // 2回目のチェック(ロックあり)
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

インスタンス生成後はロックが発生しないため高パフォーマンスです。volatileを付けることで、インスタンスの部分的な初期化問題を防ぎます。

OK例3: Initialization-on-demand holderパターン(最も推奨)

public class Singleton {
    // 内部クラスはSingleton.getInstance()が初めて呼ばれたときに初期化される
    private static class Holder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return Holder.INSTANCE;
    }
}

JVMのクラスロードの仕組みを利用した遅延初期化です。synchronized不要でスレッドセーフかつ高パフォーマンスのため、最も推奨されるパターンです。

ThreadLocalでスレッドごとに変数を分ける

ThreadLocalを使うと、各スレッドが自分専用のコピーを持てます。スレッド間でデータを共有しないため、同期化なしにスレッドセーフを実現できます。

典型的なユースケースがSimpleDateFormatです。SimpleDateFormatはスレッドセーフではないため、複数スレッドで共有すると日付のフォーマット結果が壊れることがあります。ThreadLocalを使えばスレッドごとに独立したインスタンスを保持できます。

// スレッドごとに独立したSimpleDateFormatを持つ
private static final ThreadLocal<SimpleDateFormat> dateFormat =
    ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));

public String formatDate(Date date) {
    return dateFormat.get().format(date); // 各スレッドが自分専用のインスタンスを使用
}

// スレッド終了時は必ずremove()を呼ぶ
public void cleanup() {
    dateFormat.remove(); // メモリリーク防止
}

注意点: スレッドプール環境(TomcatなどのWebサーバー)ではスレッドが再利用されます。remove()を忘れると、前のリクエストのデータが次に混入し、バグやメモリリークを起こします。使用後は必ずremove()を呼び出しましょう。

現在(Java 25時点)では、SimpleDateFormatの代わりに不変なDateTimeFormatter(Java 8以降)の使用を推奨します。DateTimeFormatterはスレッドセーフなためThreadLocalも不要です。新規プロジェクトではRecord型と組み合わせた不変設計を採用するとよりシンプルになります。

不変オブジェクト(Immutable)設計でスレッドセーフを実現する

不変(Immutable)オブジェクトは、一度生成した後に状態が変わりません。変更されないため、複数のスレッドから同時にアクセスされても安全です。同期化のコストもゼロです。

Stringクラスが安全な理由はここにあります。Stringはインスタンス生成後に値が変わらないため、スレッドセーフです。

不変クラスを設計する条件は次の4つです。

  1. すべてのフィールドをfinalにする
  2. クラスをfinalにする(継承を禁止)
  3. セッターを提供しない
  4. 可変オブジェクトへの参照をコピーして渡す(防御的コピー)
// 不変クラスの例
public final class Money {
    private final int amount;
    private final String currency;

    public Money(int amount, String currency) {
        this.amount = amount;
        this.currency = currency;
    }

    public int getAmount() { return amount; }
    public String getCurrency() { return currency; }

    // 変更が必要な場合は新しいインスタンスを返す
    public Money add(int value) {
        return new Money(this.amount + value, this.currency);
    }
}

可変クラスと不変クラスの最大の違いは「状態を変える操作」の扱いです。可変クラスはフィールドを直接書き換えますが、不変クラスは変更結果を新しいインスタンスとして返します。

Javaの主要クラスのスレッドセーフ性一覧

Javaの標準クラスにはスレッドセーフなものとそうでないものが混在します。使い分けを押さえると実行時のバグを防げます。

Javaの標準APIには、最初からスレッドセーフに設計されているクラスと、そうでないクラスがあります。

スレッドセーフなクラス(Vector・StringBuffer・ConcurrentHashMap等)

以下がスレッドセーフなクラスの例です。

  • Vector: ArrayListの古いバージョンで、すべてのパブリックメソッドにsynchronizedが付いています。そのため、ArrayListに比べてパフォーマンスが劣ります。
  • Hashtable: HashMapの古いバージョンです。パフォーマンス面でConcurrentHashMapに劣ることが多いです。
  • StringBuffer: 文字列を扱うクラスで、メソッドが同期化されています。
  • ConcurrentHashMap: 高い並行性を実現するように設計されたHashMapです。読み取り操作はロックなしで行え、書き込み操作も部分的なロックで行うため、Hashtableよりもはるかに高いパフォーマンスを発揮します。
  • CopyOnWriteArrayList: 読み取り操作が多く、書き込み操作が少ない場合に有効なリストです。書き込みが発生すると、元の配列をコピーして新しい配列を作成するため、読み取り操作はロックなしで安全に行えます。

スレッドセーフでないクラスと代替クラス(比較表)

日常的によく使う多くのクラスは、パフォーマンスを優先するため、スレッドセーフではありません。マルチスレッド環境で共有する場合は、スレッドセーフな代替クラスへの切り替えを検討してください。

非スレッドセーフスレッドセーフな代替注意点
ArrayListCopyOnWriteArrayList / Collections.synchronizedList()ArrayListはマルチスレッドでデータ破壊の危険あり
HashMapConcurrentHashMap / Collections.synchronizedMap()HashMapはマルチスレッドで無限ループに陥る可能性あり
StringBuilderStringBufferシングルスレッドならStringBuilderの方が高速
SimpleDateFormatDateTimeFormatter(Java 8以降)SimpleDateFormatは内部状態を持ちスレッドセーフでない

Collections.synchronizedXxx()でラップする方法

非スレッドセーフなコレクションを、どうしてもマルチスレッド環境で使いたい場合、Collectionsクラスのユーティリティメソッドを使って、スレッドセーフなビュー(ラッパー)を作成する方法があります。

  • Collections.synchronizedList(new ArrayList<>())
  • Collections.synchronizedMap(new HashMap<>())
  • Collections.synchronizedSet(new HashSet<>())

これらのメソッドは、元のコレクションの各メソッド呼び出しをsynchronizedブロックで囲んだ新しいコレクションを返します。

Collectionsクラスのラッパーを使えば、非スレッドセーフなコレクションを簡単にスレッドセーフ化できます。

ただし、この方法はコレクション全体をロックするため、ConcurrentHashMapなどのjava.util.concurrentパッケージのクラスに比べてパフォーマンスが劣る場合があります。

また、イテレータ(拡張for文など)を使う場合は、手動で同期化する必要がある点に注意が必要です。

List<String> list = Collections.synchronizedList(new ArrayList<>());
// ... listに要素を追加 ...

// イテレータを使う場合は、手動で同期化が必要
synchronized (list) {
    for (String item : list) {
        System.out.println(item);
    }
}

実践でよくあるスレッドセーフの落とし穴

スレッドセーフの手法を覚えても、組み合わせ方次第で問題が起きます。現場でよく見るパターンを紹介します。

スレッドセーフの基本を理解しても、実際の開発では思わぬ落とし穴にはまることがあります。ここでは、よくある問題とその対策を見ていきましょう。

複数の操作を安全に行うには?

synchronizedメソッドやスレッドセーフなクラスを使っても、複数の操作を組み合わせる場合には注意が必要です。個々の操作がスレッドセーフでも、それらを組み合わせた一連の処理がスレッドセーフであるとは限らないからです。

例えば、Vectorはスレッドセーフですが、以下のコードはスレッドセーフではありません。

Vector<String> vector = new Vector<>();
// ... vectorに要素を追加 ...

// Check-then-Act: スレッドセーフではない!①チェックと②削除の間に別のスレッドが割り込む可能性
if (!vector.isEmpty()) { // ①チェック
    String item = vector.remove(0); // ②削除
    // ... itemを使った処理 ...
}

上記のコードでは、isEmpty()で空でないことを確認してからremove(0)を呼び出しています。しかし、①のチェックと②の削除の間に、別のスレッドがvectorから最後の要素を削除してしまう可能性があります。すると、②のremove(0)は要素がないのに削除しようとして、ArrayIndexOutOfBoundsExceptionが発生します。

複数の操作を組み合わせる場合は、一連の操作全体を一つのアトミックな操作として同期化する必要があります。

synchronized (vector) { // vectorをロックして一連の処理を実行
    if (!vector.isEmpty()) {
        String item = vector.remove(0);
        // ... itemを使った処理 ...
    }
}

複数の操作を組み合わせる場合は、その一連の処理全体をsynchronizedブロックで囲む必要があります。

個々の操作が安全でも、操作の間に他のスレッドが割り込むと、前提条件が崩れる可能性があるためです。

スレッドセーフでも不十分なケースとは

スレッドセーフなクラスを使っているからといって、常に安全とは限りません。使い方によっては、やはり競合状態が発生することがあります。

例えば、「キーが存在しなければ追加する」という処理を考えてみましょう。

ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();

String key = "test";
String value = "value";

// スレッドセーフではない! ①getと②putの間に別のスレッドがputする可能性
if (map.get(key) == null) { // ①get
    map.put(key, value); // ②put
}

ConcurrentHashMapgetputはスレッドセーフですが、①と②の間はアトミックではありません。

複数の操作を組み合わせる場合は、ConcurrentHashMapが提供するアトミックなメソッドputIfAbsentを使います。

map.putIfAbsent(key, value); // キーが存在しない場合のみアトミックにputする

スレッドセーフなクラスを使う場合でも、提供されているアトミックなメソッドを適切に利用することが重要です。

自前で複数の操作を組み合わせると、意図せず競合状態を生み出す可能性があるからです。

パフォーマンスとのトレードオフ

スレッドセーフを実現するための同期化には、必ずコストがかかります。synchronizedを使うと、ロックの取得と解放にオーバーヘッドが生じ、複数のスレッドがロックを待つことで、全体の処理速度が低下します。

そのため、スレッドセーフは重要ですが、必要以上に同期化を行うことは避けるべきです。

ポイント

  • 同期化の範囲を最小限にする: synchronizedメソッドよりもsynchronizedブロックを使い、ロック範囲をできるだけ狭く。
  • ロックの粒度を考える: ConcurrentHashMapのように、オブジェクト全体ではなく部分的にロックする仕組みを利用。
  • 不変(Immutable)オブジェクトを活用する: 変更されないオブジェクトは、そもそもスレッドセーフについて心配する必要がありません。可能な限りオブジェクトを不変に設計することは、スレッドセーフなプログラムを書くための強力な戦略。
  • スレッドローカル(ThreadLocal)を使う: 各スレッドが自分専用の変数を持つようにすれば、共有による問題を避けられます。ただし、メモリ使用量が増える可能性に注意が必要。

デッドロックを防ぐための実装パターン

デッドロックは、複数のスレッドが互いに相手のロックを待ち続けて処理が止まる状態です。一度発生するとアプリケーションが応答不能になります。以下の3つのパターンで防げます。

ロック取得順序を統一する

複数のロックを取得する場合、すべてのスレッドで同じ順序でロックを取得するルールを設けます。

// NG: スレッドAはlockA→lockB、スレッドBはlockB→lockAの順でロックするとデッドロック
// OK: 常にlockA→lockBの順でロック取得する
synchronized (lockA) {
    synchronized (lockB) {
        // 処理
    }
}

ReentrantLock.tryLock()でタイムアウト制御する

ReentrantLocktryLock(timeout)を使うと、指定した時間内にロックを取得できなかった場合に諦めて処理を中断できます。デッドロックになっても自動的に解消されます。

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public boolean tryTransfer(ReentrantLock lockA, ReentrantLock lockB) throws InterruptedException {
    if (lockA.tryLock(500, TimeUnit.MILLISECONDS)) {
        try {
            if (lockB.tryLock(500, TimeUnit.MILLISECONDS)) {
                try {
                    // 両方のロックを取得できた場合のみ処理
                    return true;
                } finally {
                    lockB.unlock();
                }
            }
        } finally {
            lockA.unlock();
        }
    }
    return false; // タイムアウト → デッドロック回避
}

jstackでデッドロックを検出する

本番環境でアプリケーションが応答しなくなった場合、jstackコマンドでスレッドダンプを取得するとデッドロックの発生を確認できます。

# JavaプロセスのPIDを確認
jps -l

# スレッドダンプを取得(デッドロックは "Found one Java-level deadlock:" で確認)
jstack <PID>

SpringのBeanはシングルトン:インスタンスフィールドにリクエストデータをNG

Spring Bootのサービスクラスやコントローラーはデフォルトでシングルトンです。つまり、アプリケーション全体で1つのインスタンスを複数のリクエスト(スレッド)が共有します。

シングルトンBeanのインスタンスフィールドにリクエスト固有のデータを保持すると、高負荷時に別ユーザーのデータが混入するバグが発生します。

// NG: インスタンスフィールドにリクエストデータを持つ(シングルトンでは危険)
@Service
public class OrderService {
    private String currentUserId; // 複数スレッドで共有される → 危険!

    public void processOrder(String userId, Order order) {
        this.currentUserId = userId; // 別スレッドが上書きする可能性
        // ...
    }
}

// OK: メソッドの引数で渡す(スタック領域 = スレッド固有)
@Service
public class OrderService {
    public void processOrder(String userId, Order order) {
        // userIdはメソッド引数なのでスタック領域に置かれ、スレッドセーフ
        // ...
    }
}

リクエスト固有の情報(ユーザーID・セッション情報など)はメソッドの引数で渡すか、ThreadLocal(SpringではRequestContextHolder等)を使いましょう。

スレッドセーフの確認方法・静的解析ツールの活用

作成したコードがスレッドセーフかどうかを確認する手順を紹介します。レビューと静的解析の両方を活用しましょう。

コードレビューのチェックポイント

まず、以下の観点でコードを確認します。

  1. 共有変数の有無: インスタンス変数・クラス変数(static変数)が複数のスレッドからアクセスされていないか確認する
  2. 可変性(Mutability)の確認: 共有される変数が変更(書き込み)されるかどうか確認する。読み取り専用なら問題ない
  3. アトミック性の確認: 共有変数への複合操作(「読み込み→変更→書き込み」)が同期化されているか確認する
  4. Check-then-Actパターンの確認: 「チェックしてから操作」という処理が同期化されているか確認する

静的解析ツールによるチェック

ツールを使って自動的にスレッドセーフの問題を検出することもできます。

  • SpotBugs(旧FindBugs、最新版: 4.9.8 / 2025年10月リリース): Javaのバグパターンを静的解析するツール。スレッドセーフに関するバグパターン(IS2_INCONSISTENT_SYNC: 同期化の不一致など)を検出できます。MavenプラグインやGradle、IntelliJ IDEAのプラグインとして利用可能。
  • IntelliJ IDEAのスレッドセーフ警告: @GuardedByアノテーションを使うと、フィールドへのアクセスが適切にロックされているかをIDEが警告してくれます。
import javax.annotation.concurrent.GuardedBy;

public class SafeCounter {
    @GuardedBy("this") // thisロックで保護されていることを明示
    private int count = 0;

    public synchronized void increment() {
        count++;
    }
}

Java 21以降の仮想スレッド(Virtual Thread)とスレッドセーフ

Java 21(2023年9月、LTS)でProject Loom由来の仮想スレッド(Virtual Thread)が正式採用されました。2025年9月にはJava 25(LTS)がリリースされ、仮想スレッドはさらに安定しています。2026年3月時点の最新版はJava 26です。

仮想スレッドとは

仮想スレッドはJVMが管理する軽量なスレッドです。数万〜数百万の仮想スレッドを低コストで作成できるため、I/O待ちが多いWebアプリケーションで特に効果的です。

// 仮想スレッドの起動(Java 21以降)
Thread.ofVirtual().start(() -> {
    System.out.println("仮想スレッドで実行中: " + Thread.currentThread().isVirtual());
});

// ExecutorServiceでの使用
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    executor.submit(() -> System.out.println("タスク実行"));
}

スレッドセーフの考え方は従来と同じ

仮想スレッドを使っても、スレッドセーフの基本原則は変わりません。ヒープ領域の共有データへの同時アクセスは依然として危険です。synchronizedvolatileAtomicクラスを適切に使う必要があります。

synchronizedとのピン留め問題に注意

仮想スレッドがsynchronizedブロック内でI/O待ちになると、仮想スレッドがOSスレッド(キャリアスレッド)に「ピン留め」されます。この状態では仮想スレッドのメリット(軽量なI/O待機)が失われます。

新規プロジェクトで仮想スレッドを活用する場合は、synchronizedの代わりにReentrantLockを使うことでピン留めを回避できます。Java 24以降ではsynchronizedのピン留め問題が改善されています。

まとめと学びを深めるポイント

スレッドセーフは設計段階で決まります。後付けの同期化は破綻しやすいため、共有状態を最小化することが鉄則です。最後に、判断の視点・開発時の意識・参考資料を紹介します。

スレッドセーフを判断する3つの視点

あるコードがスレッドセーフかどうかを考える際には、以下の3つの視点が役立ちます。

  1. 状態の共有 (Shared State): 複数のスレッドからアクセスされる可能性のあるデータ(インスタンス変数、静的変数など)はあるか? 共有されているデータがなければ、通常スレッドセーフの問題は起きない。
  2. 可変性 (Mutability): 共有されているデータは変更されるか? データが不変(読み取り専用)であれば、複数のスレッドから同時にアクセスされても問題ない。
  3. アトミック性 (Atomicity): 共有されている可変データへのアクセス(特に読み書きを伴う一連の操作)は、アトミックに行われているか? synchronizedAtomicクラスなどで保護されているか確認。

開発時に意識したいこと

スレッドセーフに関して開発時に意識したいことは以下のとおりです。

  • 設計段階から考慮する: マルチスレッドを後から付け足すのは困難のため、最初からスレッドセーフを意識して設計する。
  • 共有するデータは最小限に: 可能な限り、スレッド間でデータを共有しない設計をする。
  • 不変性を活用する: 変更する必要のないデータは、final修飾子を使うなどして不変にする。
  • 適切な同期化手法を選ぶ: 状況に応じてsynchronized, volatile, Lock, Atomicクラス、Concurrentコレクションを使い分ける。
  • テストを徹底する: マルチスレッドのバグは再現が難しいため、負荷テストと並行処理を想定したテストが必須です。
  • Java 25(LTS)・Java 26でVirtual Threadはさらに安定: Java 21でVirtual Threadが正式採用されました。最新LTSはJava 25(2025年9月)、最新版はJava 26(2026年3月)です。新規プロジェクトでの活用を検討しましょう。スレッドセーフの考え方は従来スレッドと変わりません。

おすすめの参考資料・書籍

さらにJavaのスレッドセーフや並行処理について深く学びたい方には、以下の資料がおすすめです。

『Java並行処理プログラミング ―その「基盤」と「最新API」を究める―』はJavaの並行処理に関するバイブル的な一冊。java.util.concurrentパッケージの作者自身による解説で、理論から実践まで深く学べます。難易度は高めですが、読む価値があります。

この記事で学んだこと(確認リスト)

  • □ スレッドセーフとは何かを自分の言葉で説明できる
  • □ synchronized・volatile・AtomicIntegerの使い分けがわかる
  • □ シングルトンパターンのスレッドセーフな実装方法(3パターン)を知っている
  • □ よくある落とし穴(複数操作の非アトミック性・Spring Beanの注意点)を把握している
  • □ SpotBugsを使ったスレッドセーフの静的解析方法がわかる
  • この記事を書いた人
  • 最新記事

トム

・都内自社開発企業勤務/Javaバックエンドエンジニア
/Java歴10年以上 ・首都圏在住30代
・資格:基本情報技術者/応用情報技術者/Java Silver/Python3エンジニア認定基礎 詳細なプロフィール

-Java入門