Javaにおける並列処理:よくある面接問題の核心ポイント

スレッドの生成とライフサイクル

Javaにおけるスレッドは、複数の方法で生成可能です。それぞれの方法には特徴があり、用途に応じて使い分ける必要があります。

  • Threadクラスを継承:run()メソッドをオーバーライドしてスレッド動作を定義します。
class CustomThread extends Thread {
    public void run() {
        // スレッド実行内容
    }
}

// スレッド開始
CustomThread thread = new CustomThread();
thread.start();
  • Runnableインターフェースを実装:run()メソッドを提供し、Threadクラスと分離できます。
class Task implements Runnable {
    public void run() {
        // タスク処理
    }
}

// スレッド作成と実行
Task task = new Task();
Thread thread = new Thread(task);
thread.start();
  • Callableインターフェースを使用:戻り値を持つタスクを定義できます。
class ResultTask implements Callable<String> {
    public String call() {
        return "完了";
    }
}

ResultTask task = new ResultTask();
FutureTask<String> future = new FutureTask<>(task);
Thread thread = new Thread(future);
thread.start();
  • スレッドプール経由:ExecutorServiceでスレッド管理を効率化します。
ExecutorService pool = Executors.newFixedThreadPool(3);
pool.submit(() -> {
    // タスク内容
});
pool.shutdown();

スレッドの状態遷移:

  1. 新規作成(NEW)
  2. 実行可能(RUNNABLE)
  3. 実行中(RUNNING)
  4. 待機中(WAITING/TIMED_WAITING)
  5. ブロック中(BLOCKED)
  6. 終了(TERMINATED)

synchronizedの内部メカニズム

排他制御にはsynchronizedキーワードが使用され、JVMレベルのロック機構に基づいています。

  • 利点:ロックの段階的昇格により、競合が少ない場合のパフォーマンスを最適化します。
  • 欠点:ロック粒度が広いと競合が発生しやすくなります。

ロックの昇格プロセス:

  • 無ロック状態
  • 偏向ロック(一度しかアクセスがない場合)
  • 軽量ロック(複数スレッドによる競合)
  • 重量ロック(スレッドが多く競合する場合)

ダブルチェックシングルトンの実装

シングルトンインスタンスの初期化には、スレッドセーフな方法が必要です。

public class Singleton {
    private static volatile Singleton uniqueInstance;

    private Singleton() {}

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

volatileの必要性:インスタンス生成時の命令の再順序化を防ぎ、完全に初期化されたオブジェクトを確実に返します。

AbstractQueuedSynchronizer(AQS)の仕組み

AQSは、Javaの同期処理の基盤となるフレームワークです。ReentrantLockやCountDownLatchなどの基底クラスとして利用されます。

  • state変数:現在のロックの取得状況を示します。
  • Node双方向キュー:ロックを待つスレッドを管理します。

ReentrantLockでの使用例:

ReentrantLock lock = new ReentrantLock();

new Thread(() -> {
    lock.lock();
    try {
        // 排他処理
    } finally {
        lock.unlock();
    }
}).start();

スレッドプールの活用方法

スレッドプールの主な種類:

  • 固定サイズスレッドプール:Executors.newFixedThreadPool()
  • キャッシュ可能スレッドプール:Executors.newCachedThreadPool()
  • 単一スレッドプール:Executors.newSingleThreadExecutor()
  • スケジュール付きスレッドプール:Executors.newScheduledThreadPool()

Fork/Joinフレームワークの活用

大規模タスクを分割して並列実行するフレームワークです。

class SumTask extends RecursiveTask<Integer> {
    private final int[] array;
    private final int start;
    private final int end;

    public SumTask(int[] array, int start, int end) {
        this.array = array;
        this.start = start;
        this.end = end;
    }

    protected Integer compute() {
        if (end - start <= 10) {
            return Arrays.stream(array, start, end).sum();
        } else {
            int mid = (start + end) / 2;
            SumTask left = new SumTask(array, start, mid);
            SumTask right = new SumTask(array, mid, end);
            left.fork();
            right.fork();
            return left.join() + right.join();
        }
    }
}

タグ: Java スレッド 並列処理 ForkJoinPool AQS

7月22日 22:24 投稿