Java 並行処理におけるスレッドセーフティと同期機構の深層分析

1. volatile キーワードによるメモリモデル制御

volatile はマルチスレッド環境下において、共有変数の「可視性」を担保するための専用修飾子です。ある変数を volatile として宣言した場合、各スレッドはこの変数にアクセスする際にキャッシュされた値ではなく、最新の値を必ずメインメモリから読み取るよう強制されます。これにより、一方のスレッドが変更を加えた際、他のすべてのスレッドが即座にその変化を認識できるようになります。

さらに、volatile は「メモリの順序制約」を提供し、コンパイラやプロセッサによる最適化(命令のリオーダー)がプログラムのロジックを破綻させるのを防ぎます。主な機能は以下の 2 点に集約されます。

  • 主メモリへの即時書き込み保証(可視性の確保)
  • 特定のパターンでの命令実行順序維持(並べ替えの禁止)

シングルトンパターンの二重チェックロック事例

遅延初期化を行うシングルトンパターンにおいて、volatile はインスタンスの不完全な初期化を防ぐために不可欠です。以下は構成管理クラスを用いた改良例です。

public class GlobalConfigRegistry {
    // volatile 指定により、参照の再構築順序と可視性を保証する
    private static volatile GlobalConfigRegistry configHandler = null;

    private GlobalConfigRegistry() {
        // 外部からのインスタンス化を防止
    }

    public static GlobalConfigRegistry obtainInstance() {
        // 初回チェック:既に生成済みなら同期処理を回避
        if (configHandler == null) { 
            synchronized (GlobalConfigRegistry.class) {
                // 二回目のチェック:進入中のスレッドを待避させるため
                if (configHandler == null) {
                    configHandler = new GlobalConfigRegistry();
                }
            }
        }
        return configHandler;
    }
}

ボラタイルの動作原理

可視性の担保: volatile の書込み操作には LOCK プレフィックス命令が含まれます。これにより、CPU のキャッシュラインが一時的にロックされ、変更データが直ちにメインメモリへ反映されます。また、MESI プロトコルを通じて他 CPU コアのキャッシュが無効化され、結果として他スレッドが更新後の値を取得します。

順序制約(メモリバリア): JMM(Java Memory Model)は volatile アクセスの前後にメモリバリアを挿入します。例えば、書込み前に StoreStore バリアを入れ、読込後に LoadLoad バリアを入れることで、特定のメモリ操作が前後に入れ替わらないように制限します。

注意: volatile は可視性と順序性は保ちますが、インクリメントのような複合演算の原子性は保証しません。原子性が必要な場合は synchronized または Lock を併用する必要があります。


2. synchronized による同期メカニズム

synchronized は Java が提供する標準的な排他制御キーワードです。これはコードブロックやメソッドに対して適用され、同一时刻に単一のスレッドのみが対象のリソースへアクセスできることを保証します。

使用形態の違い

1. インスタンスメソッド修饰: 対象となるのは呼び出し元インスタンス自身(current object)です。

// このメソッドが実行される間、this オブジェクトの監視器が占有される
public synchronized void processRequest() {
    // クリティカルセクション
}

2. 静的メソッド修饰: 対象となるのは該クラスの Class オブジェクトです。

// クラスローダレベルでロックが取得される
public static synchronized void initSystem() {
    // クラス初期化処理
}

3. クラスコードブロック: 任意のオブジェクトあるいは Class オブジェクトを明示的にロックターゲットとする。

public void updateData(Object resource) {
    synchronized(resource) {
        // リソースに対する排他処理
    }
}
// またはクラス単位で
synchronized(MyClass.class) { ... }

重要な注意点

  • 静的 synchronized メソッドと非静的 synchronized メソッドは異なるロックオブジェクトを使用するため、互いに排他的ではありません。
  • synchronized(String s) は文字列プールによる共通参照の影響を受け危険であるため、推奨されません。
  • コンストラクタ自体はスレッドセーフな文脈で呼ばれる前提があるため、通常 synchronized で囲む必要はありません。

volatile と synchronized の比較

両者は補完関係にあります。volatile は軽量で可視性に特化しますが、原子性は持ちません。一方、synchronized は可視性・原子性ともに保証しますが、重量級でありコンテキストスイッチのコストがかかります。

内部実装:モニターオブジェクト

JVM のレベルでは、各オブジェクトヘッダーには「モノライザー(モニタ)」へのポインタが含まれています。スレッドは CAS 演算により所有権を獲得し、成功すれば所有権フラグを設定します。失敗した場合はエントリーリスト(EntryList)へキューイングされ、待機状態となります。notify 呼び出し時にウィットセット(WaitSet)内のスレッドが再開されます。


3. Lock インターフェースと ReentrantLock の特徴

Lock インターフェースは、より柔軟なロック操作を API レベルで提供します。synchronized が JVM ネイティブであるのに対し、ReentrantLock は JDK クラスライブラリの実装です。

主要な拡張機能

synchronized に比べ、ReentrantLock は以下の高度な制御が可能です。

  1. 待機の割り込み: lockInterruptibly() を利用することで、待機中のスレッドがタイムアウトや中断要求に対して応答させられます。
  2. 公平性の選択: コンストラクタで new ReentrantLock(true) とすることで、先に待機したスレッドが優先される公平モードを設定可能です(デフォルトは非公平)。
  3. 複数の条件変数: Condition インターフェースを使い、await()signal() によって複雑な待機・通知のシナリオを実装できます。
import java.util.concurrent.locks.ReentrantLock;

public class TaskProcessor {
    private final ReentrantLock taskLock = new ReentrantLock(false);
    
    public void execute() {
        taskLock.lock();
        try {
            // 処理ロジック
            doTask();
        } finally {
            // 必ず解放する
            taskLock.unlock();
        }
    }
}

4. CAS と原子性および ABA 問題

CAS(Compare And Swap) は、不揮発性メモリ上の値を対象に、「現在値」と「期待値」が一致する場合のみ新しい値に上書きする、アトミックな操作です。オペレーティングシステムや CPU アーキテクチャによってサポートされており、ソフトウェアロック(Mutex)よりもオーバーヘッドが低い非ブロッキングアルゴリズムの基礎となります。

CAS の実行フロー

  1. メインメモリから現在の値を読み取る。
  2. 計算を行い、期待値(元の値)と比較する。
  3. 一致していれば、メモリ位置へ新値を書き込む(成功)。
  4. 不一致の場合、何もしない、または自旋して再試行する(失敗)。

ABA 問題とその対策

CAS 操作において、値が A から B に変わり、再び A に戻っていた場合、単純な CAS 比較では「変化がない」と誤判定されてしまいます。これを ABA 問題と呼びます。

この問題を解決するには、値に加えてバージョン番号を管理する必要があります。AtomicStampedReference クラスは、参照先オブジェクトと同様に改訂番号も同時に格納・比較するため、値が戻っても版本号が変わっていれば不一致として検知できます。


5. AQS 設計思想と同期フレームワーク

AQS(AbstractQueuedSynchronizer) は、Java 並行パッケージ(JUC)の根幹にある抽象クラスです。多くの高水準同期ツール(Semaphore, CountDownLatch, ReentrantLock など)が AQS を基盤として構築されています。

核心的な設計

AQS は、内部状態(state)の変数と FIFO(先入先出)キューを使用して、スレッドの同期を管理します。

  • 独占モード: 1 つのステートしか保持されないモデル。典型的なのはリード/ライトロックや排他ロック。ReentrantLock が該当します。
  • 共有モード: 複数スレッドが同時に応答可能なモデル。Semaphore(シマフョア)や CountDownLatch が該当します。

動作メカニズム

スレッドがリソースを獲得しようとした際、内部的な state 値と CAS 演算で検証を行います。獲得できないスレッドはノードとしてキューの末尾に追加され、パーク(一時停止)状態になります。保有者がリソースを解放すると、キューの先頭から次のスレッドが唤醒され、再度獲得を試みます。このメカニズムにより、効率的かつスケーラブルな同期制御が可能になります。

タグ: Java concurrency synchronization volatile cas

5月19日 12:48 投稿