Java マルチスレッド通信と同期制御: 単一インスタンス生成、デッドロック回避、線程制御手法

単一インスタンス生成における競合回避:synchronized と volatile の活用 以下の遅延初期化(ラザイン)型 singleton 実装では、複数スレッドが同時に getInstance() を呼び出した際に、2つ以上のインスタンスが生成されてしまう問題があります:

public final class Singleton {
    private static Singleton instance = null;
    private Singleton() {}
    
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton(); // ← 競合発生リスクあり
        }
        return instance;
    }
}

この問題を解決する基本的手法として、クラスロックによる排他制御があります。static メソッドに synchronized を適用するか、明示的に Singleton.class をロック対象とする必要があります:

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

ただし、上記実装では毎回ロック処理が実行され、オーバーヘッドが大きくなります。そこで、ダブルチェックロック(DCL)方式を用いることで、高速化を図ります:

public final class Singleton {
    private static Singleton instance = null;
    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) { // ← 第1チェック:高速パス
            synchronized (Singleton.class) {
                if (instance == null) { // ← 第2チェック:ネスト内排他
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

しかし、このままでは instance = new Singleton() の命令再配列(instruction reordering)により、インスタンス参照が初期化前に visible になる可能性があります。 これに対処するため、volatile 修飾子を用いて、変数の可視性と順序保証を強制します:

public final class Singleton {
    private static volatile Singleton instance = null;
    private Singleton() {}

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

synchronizedvolatile の動作差異

synchronized: 互斥 access を保証(原子性)、さらにスレッド間での variable の可視性を同期(可視性保証)。ただしロック取得・解放にコストあり。 volatile: 変数への read/write を主記憶と常時同期し、命令再配列を禁止(順序性保証)。原子性は保証されない(例: i++ は非原子的)。

finalstatic の役割

final: クラス継承禁止・メソッド override 禁止・変数再代入禁止を意味。不変性の保証に有用。 static: クラスロード時に1つだけ確保される変数/メソッド。インスタンス間で共有される(ただし、可変であれば同期制御が必要)。

ロック機構の内部実装:CAS と Thread Park 実際のロック機構では、以下のコンポーネントが協調して動作します:

所有者保持: AtomicReference<Thread>volatile で宣言し、排他所有スレッドを保持。 排他取得(CAS):

private volatile AtomicReference<Thread> owner = new AtomicReference<>();

public void lock() {
    if (!owner.compareAndSet(null, Thread.currentThread())) {
        // CAS失敗 → 待ち行列へ
        waiters.add(Thread.currentThread());
        LockSupport.park(this);
    }
}

解除処理:

public void unlock() {
    if (owner.compareAndSet(Thread.currentThread(), null)) {
        Thread next = waiters.poll();
        if (next != null) {
            LockSupport.unpark(next);
        }
    }
}

スレッド制御: 交互出力実装 スレッドAとBを交互に実行させるには、以下2つの方法があります:

  1. synchronized + wait/notify 方式
public class AlternatingPrinter {
    private static final Object lock = new Object();
    private static boolean turnA = true;

    public static void main(String[] args) {
        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                synchronized (lock) {
                    while (!turnA) {
                        try { lock.wait(); } catch (InterruptedException e) {}
                    }
                    System.out.print("A");
                    turnA = false;
                    lock.notifyAll();
                }
            }
        }).start();

        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                synchronized (lock) {
                    while (turnA) {
                        try { lock.wait(); } catch (InterruptedException e) {}
                    }
                    System.out.print("B");
                    turnA = true;
                    lock.notifyAll();
                }
            }
        }).start();
    }
}
  1. ReentrantLock + Condition 方式
public class AlternatingWithCondition {
    private static final ReentrantLock lock = new ReentrantLock();
    private static final Condition condA = lock.newCondition();
    private static final Condition condB = lock.newCondition();
    private static boolean turnA = true;

    public static void main(String[] args) {
        new Thread(() -> {
            lock.lock();
            try {
                for (int i = 0; i < 5; i++) {
                    while (!turnA) condA.await();
                    System.out.print("A");
                    turnA = false;
                    condB.signal();
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } finally {
                lock.unlock();
            }
        }).start();

        new Thread(() -> {
            lock.lock();
            try {
                for (int i = 0; i < 5; i++) {
                    while (turnA) condB.await();
                    System.out.print("B");
                    turnA = true;
                    condA.signal();
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } finally {
                lock.unlock();
            }
        }).start();
    }
}

デッドロックの発生例と回避方針 2つのロックを異なる順序で取得すると、デッドロックの条件が揃います:

public class DeadlockDemo {
    public static void main(String[] args) {
        final Object lock1 = new Object();
        final Object lock2 = new Object();

        Thread t1 = new Thread(() -> {
            synchronized (lock1) {
                try { Thread.sleep(100); } catch (InterruptedException _) {}
                synchronized (lock2) {
                    System.out.println("t1 acquired both locks");
                }
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (lock2) {
                try { Thread.sleep(100); } catch (InterruptedException _) {}
                synchronized (lock1) {
                    System.out.println("t2 acquired both locks");
                }
            }
        });

        t1.start();
        t2.start();
    }
}

回避策としては、 ロック取得順序の統一、 タイムアウト付きロック取得(tryLock())、 ロック順序強制(ドメインによるID割付)などが有効です。

Java スレッド実装の3手法比較

Thread継承: 単純だがJavaScriptとの違いで多重継承不可の制約。 Runnable実装: 処理と担当スレッドを分離可能。资源共享が簡単に実現可能。 Callable実装 + FutureTask: 戻り値返却・例外伝搬をサポート。非同期処理の成果物収集に最適。

例実装:

public class ThreadImplementations {
    public static void main(String[] args) throws Exception {
        // 1. Thread継承
        new MyThreadExt().start();

        // 2. Runnable実装
        new Thread(new MyRunnableImp()).start();

        // 3. Callable実装
        FutureTask<String> task = new FutureTask<>(new MyCallableImp());
        new Thread(task).start();
        System.out.println("Result: " + task.get());
    }
}

class MyThreadExt extends Thread {
    public void run() { System.out.println("Extends Thread"); }
}

class MyRunnableImp implements Runnable {
    public void run() { System.out.println("Implements Runnable"); }
}

class MyCallableImp implements Callable<String> {
    public String call() { return "Callable returning value"; }
}

タグ: Java synchronized volatile lock-free cas

8月13日 02:23 投稿