単一インスタンス生成における競合回避: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;
}
}
synchronized と volatile の動作差異
synchronized: 互斥 access を保証(原子性)、さらにスレッド間での variable の可視性を同期(可視性保証)。ただしロック取得・解放にコストあり。
volatile: 変数への read/write を主記憶と常時同期し、命令再配列を禁止(順序性保証)。原子性は保証されない(例: i++ は非原子的)。
final と static の役割
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つの方法があります:
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();
}
}
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"; }
}