【Java入門シリーズ】第14章: Javaのマルチスレッド 〜並行処理で効率的なプログラムを作る〜

【Java入門シリーズ】第14章 Javaのマルチスレッド 〜並行処理で効率的なプログラムを作る〜 JAVA
PR

現代のコンピュータでは、複数の処理を同時に行うことが求められます。これを実現するための仕組みがマルチスレッドです。Javaは、強力なマルチスレッドサポートを提供しており、複数のタスクを同時に実行することで、プログラムのパフォーマンスを大幅に向上させることができます。特に、複雑な計算やI/O処理、リアルタイム処理を行う際に、マルチスレッドを活用することが重要です。

この章では、Javaのマルチスレッドプログラミングの基本から、スレッドの作成と管理、同期の仕組み、デッドロック回避、スレッドプールといった高度なトピックまでを解説します。

具体的には、以下の内容をカバーします:

  • スレッドの基本
  • スレッドの作成方法
  • スレッドの同期と排他制御
  • デッドロックとその回避
  • スレッドプールの利用

14.1 マルチスレッドとは?

スレッドとは、プログラム内で実行される処理の最小単位を指します。Javaプログラムはデフォルトではシングルスレッド(単一のスレッド)で動作しますが、マルチスレッドを使用することで、複数のスレッドが同時に実行されるようになります。

例えば、1つのスレッドで計算を行いながら、もう1つのスレッドでファイルの読み込みを並行して行うことができます。これにより、プログラムの応答性やパフォーマンスを向上させることができます。

14.1.1 マルチスレッドの利点

  • 並行処理の実現: 複数のタスクを同時に実行することで、時間を節約し、処理効率を高めることができる。
  • リソースの効率的な活用: CPUやメモリなどのリソースを最大限に活用できる。
  • 応答性の向上: ユーザーインターフェースのプログラムなどでは、バックグラウンドで処理を行いながら、メインスレッドでの応答を維持することが可能。

14.2 Javaにおけるスレッドの作成方法

Javaでは、スレッドを作成して実行するために、2つの主な方法が用意されています:

  1. Threadクラスを継承する方法
  2. Runnableインターフェースを実装する方法

14.2.1 Threadクラスを継承する方法

Threadクラスを継承してスレッドを作成する方法は、最も基本的なアプローチの1つです。この方法では、Threadクラスのrun()メソッドをオーバーライドして、実行したい処理を記述します。

// Threadクラスを継承してスレッドを定義
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(getName() + "の実行: " + i);
}
}
}

public class Main {
public static void main(String[] args) {
// スレッドのインスタンスを作成
MyThread thread1 = new MyThread();
MyThread thread2 = new MyThread();

// スレッドの開始
thread1.start();
thread2.start();
}
}

このコードでは、MyThreadクラスがThreadクラスを継承しており、2つのスレッドが同時に実行されます。start()メソッドを呼び出すことで、スレッドが起動し、run()メソッドの処理が実行されます。

14.2.2 Runnableインターフェースを実装する方法

より柔軟なスレッドの作成方法として、Runnableインターフェースを実装する方法があります。この方法では、Threadクラスを継承せずに、Runnableインターフェースのrun()メソッドを実装します。

// Runnableインターフェースを実装
class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "の実行: " + i);
}
}
}

public class Main {
public static void main(String[] args) {
// Runnableを実装したクラスのインスタンスを作成
MyRunnable runnable = new MyRunnable();

// ThreadクラスのコンストラクタにRunnableを渡してスレッドを作成
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);

// スレッドの開始
thread1.start();
thread2.start();
}
}

Runnableインターフェースを使うことで、クラスの多重継承が可能となり、柔軟な設計が可能です。また、スレッドに渡す処理を明示的に分離できるため、コードが整理されやすくなります。

14.2.3 ラムダ式を使ったスレッドの作成

Java 8以降、Runnableインターフェースをラムダ式で簡潔に表現することができます。

public class Main {
public static void main(String[] args) {
// ラムダ式でRunnableを実装してスレッドを作成
Thread thread = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "の実行: " + i);
}
});

// スレッドの開始
thread.start();
}
}

ラムダ式を使うことで、スレッドの作成がさらに簡潔になります。


14.3 スレッドの同期と排他制御

マルチスレッドプログラミングでは、複数のスレッドが同時に同じリソースにアクセスすることがあり、競合状態(Race Condition)が発生する可能性があります。これを避けるために、スレッド間の同期が必要です。

Javaでは、synchronizedキーワードを使って、複数のスレッドが同時にリソースにアクセスしないようにする排他制御が提供されています。

14.3.1 synchronizedキーワードの使い方

synchronizedキーワードを使うことで、特定のメソッドやブロックをクリティカルセクションとして指定し、その部分に同時にアクセスできるスレッドを1つに制限します。

class Counter {
private int count = 0;

// synchronizedメソッドで排他制御
public synchronized void increment() {
count++;
}

public int getCount() {
return count;
}
}

public class Main {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();

// 複数のスレッドでカウンタを同時に操作
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});

Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});

thread1.start();
thread2.start();

// スレッドの終了を待機
thread1.join();
thread2.join();

System.out.println("カウンタの値: " + counter.getCount());
}
}

このコードでは、increment()メソッドがsynchronizedで保護されているため、複数のスレッドが同時にこのメソッドにアクセスすることが防がれ、正しいカウンタの値が保証されます。

14.3.2 synchronizedブロックの使い方

メソッド全体を同期するのではなく、特定のブロックだけを同期する場合は、synchronizedブロックを使います。

class Counter {
private int count = 0;

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

public int getCount() {
return count;
}
}

14.4 デッドロックとその回避

デッドロックは、2つ以上のスレッドがそれぞれ相手の持つリソースを待ち続ける状況です。この状態になると、全てのスレッドが停止してしまい、プログラムが動かなくなります。デッドロックを回避するためには、適切な設計とリソース管理が必要です。

14.4.1 デッドロックの例

以下のコードは、2つのスレッドがデッドロックに陥る例です。

class Resource {
public synchronized void methodA(Resource other) {
System.out.println("Method A is running...");
other.methodB();
}

public synchronized void methodB() {
System.out.println("Method B is running...");
}
}

public class Main {
public static void main(String[] args) {
Resource resource1 = new Resource();
Resource resource2 = new Resource();

// スレッド1がresource1のmethodAを呼び出し、resource2を待機
Thread thread1 = new Thread(() -> resource1.methodA(resource2));

// スレッド2がresource2のmethodAを呼び出し、resource1を待機
Thread thread2 = new Thread(() -> resource2.methodA(resource1));

thread1.start();
thread2.start();
}
}

この例では、スレッド1とスレッド2が互いに相手のリソースを待ち続けることで、デッドロックが発生します。

14.4.2 デッドロック回避策

デッドロックを回避するための一般的な方法には、次のようなものがあります:

  • リソース取得の順序を統一することで、競合の可能性をなくす。
  • タイムアウトを設定し、スレッドが特定の時間内にリソースを取得できなかった場合に中断させる。
  • 手動でリソースの競合を管理し、デッドロックを予防するロジックを設計する。

14.5 スレッドプール

マルチスレッドの処理では、頻繁にスレッドを作成・破棄することがパフォーマンスに悪影響を与える可能性があります。そこで、スレッドプールを利用することで、スレッドの再利用と管理を効率化できます。

14.5.1 ExecutorServiceを使ったスレッドプールの作成

Javaでは、ExecutorServiceインターフェースを使ってスレッドプールを管理できます。これにより、スレッドの再利用やタスクのキュー処理が可能です。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Main {
public static void main(String[] args) {
// スレッドプールを作成(固定サイズのプール)
ExecutorService executor = Executors.newFixedThreadPool(3);

// タスクをスレッドプールに送信
for (int i = 0; i < 5; i++) {
final int taskNumber = i;
executor.submit(() -> {
System.out.println("タスク " + taskNumber + " を実行中: " +
Thread.currentThread().getName());
});
}

// スレッドプールを終了
executor.shutdown();
}
}

このコードでは、3つのスレッドを持つスレッドプールが作成され、5つのタスクが順次実行されます。スレッドプールは、タスクの並列処理やスレッドの効率的な管理に最適です。


14.6 Javaのマルチスレッドまとめ

この章では、Javaにおけるマルチスレッドの基本から、スレッドの作成方法、スレッド間の同期、デッドロックの回避、そしてスレッドプールの利用までを学びました。マルチスレッドプログラミングは、効率的な並行処理を実現し、複雑なタスクを効率的に処理するために欠かせない技術です。