スレッド

Java – スレッドの使い方を初心者向けにわかりやすく解説

Javaでスレッドを使う方法は主に2つあります。

1つ目はThreadクラスを継承し、runメソッドをオーバーライドする方法です。

2つ目はRunnableインターフェースを実装し、runメソッドを定義してThreadに渡す方法です。

後者は柔軟性が高く推奨されます。

スレッドを開始するにはstartメソッドを呼びます。

スレッドは並行処理を実現するために使われ、例えば複数のタスクを同時に実行したい場合に便利です。

スレッドとは何か

スレッドは、プログラムの中で独立して実行される処理の単位です。

Javaでは、スレッドを使用することで、複数の処理を同時に実行することが可能になります。

これにより、アプリケーションの応答性を向上させたり、リソースを効率的に利用したりすることができます。

スレッドの特徴

  • 並行処理: 複数のスレッドが同時に実行されることで、処理の効率が向上します。
  • リソースの共有: スレッドは同じメモリ空間を共有するため、データのやり取りが容易です。
  • 軽量性: スレッドはプロセスよりも軽量で、作成や管理が比較的簡単です。

スレッドの種類

スレッドの種類説明
ユーザースレッドアプリケーションが作成するスレッド。通常の処理を行う。
デーモンスレッドバックグラウンドで動作するスレッド。ユーザースレッドが全て終了すると自動的に終了する。

スレッドを利用することで、アプリケーションのパフォーマンスを向上させることができますが、適切な管理が求められます。

次のセクションでは、Javaでスレッドを作成する方法について解説します。

Javaでスレッドを作成する方法

Javaでは、スレッドを作成する方法が主に2つあります。

1つはThreadクラスを継承する方法、もう1つはRunnableインターフェースを実装する方法です。

それぞれの方法について詳しく解説します。

Threadクラスを継承する方法

Threadクラスを継承して、runメソッドをオーバーライドすることでスレッドを作成します。

以下はそのサンプルコードです。

// App.java
public class App extends Thread {
    @Override
    public void run() {
        // スレッドが実行する処理
        for (int i = 0; i < 5; i++) {
            System.out.println("スレッドからのメッセージ: " + i);
            try {
                // 1秒待機
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {
        // スレッドのインスタンスを作成
        App thread = new App();
        // スレッドを開始
        thread.start();
    }
}

このコードでは、AppクラスがThreadクラスを継承し、runメソッド内でスレッドが実行する処理を定義しています。

mainメソッドでは、スレッドのインスタンスを作成し、startメソッドを呼び出すことでスレッドを開始します。

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

Runnableインターフェースを実装することで、スレッドを作成することもできます。

この方法では、Runnableを実装したクラスのインスタンスをThreadクラスに渡してスレッドを作成します。

以下はそのサンプルコードです。

// App.java
public class App implements Runnable {
    @Override
    public void run() {
        // スレッドが実行する処理
        for (int i = 0; i < 5; i++) {
            System.out.println("Runnableからのメッセージ: " + i);
            try {
                // 1秒待機
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {
        // Runnableを実装したインスタンスを作成
        App runnable = new App();
        // Threadクラスのインスタンスを作成
        Thread thread = new Thread(runnable);
        // スレッドを開始
        thread.start();
    }
}

このコードでは、AppクラスがRunnableインターフェースを実装し、runメソッド内でスレッドが実行する処理を定義しています。

mainメソッドでは、Runnableを実装したインスタンスを作成し、それをThreadクラスに渡してスレッドを開始します。

  • Threadクラスを継承する方法とRunnableインターフェースを実装する方法の2つがある。
  • どちらの方法でも、runメソッド内にスレッドが実行する処理を記述する。

次のセクションでは、スレッドの制御方法について解説します。

スレッドの制御方法

Javaでは、スレッドの実行を制御するためのさまざまなメソッドが用意されています。

これにより、スレッドの開始、停止、一時停止、再開などを行うことができます。

以下に、主なスレッド制御メソッドを紹介します。

スレッドの開始

スレッドを開始するには、start()メソッドを使用します。

このメソッドを呼び出すことで、スレッドが新しい実行の流れを持ち、run()メソッドが実行されます。

スレッドの一時停止

スレッドを一時的に停止させるには、sleep(long millis)メソッドを使用します。

このメソッドは、指定したミリ秒数だけスレッドを停止します。

以下はそのサンプルコードです。

// App.java
public class App extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("スレッドからのメッセージ: " + i);
            try {
                // 1秒待機
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {
        App thread = new App();
        thread.start();
    }
}

スレッドの停止

スレッドを停止させるには、interrupt()メソッドを使用します。

このメソッドを呼び出すことで、スレッドが中断され、InterruptedExceptionがスローされることがあります。

以下はそのサンプルコードです。

// App.java
public class App extends Thread {
    @Override
    public void run() {
        try {
            for (int i = 0; i < 5; i++) {
                System.out.println("スレッドからのメッセージ: " + i);
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            System.out.println("スレッドが中断されました。");
        }
    }
    public static void main(String[] args) {
        App thread = new App();
        thread.start();
        try {
            // 3秒後にスレッドを中断
            Thread.sleep(3000);
            thread.interrupt();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

スレッドの優先度

スレッドの優先度を設定するには、setPriority(int priority)メソッドを使用します。

優先度は1から10の範囲で設定でき、数値が大きいほど優先度が高くなります。

以下はそのサンプルコードです。

// App.java
public class App extends Thread {
    @Override
    public void run() {
        System.out.println("スレッドの優先度: " + getPriority());
    }
    public static void main(String[] args) {
        App thread1 = new App();
        App thread2 = new App();
        
        // スレッドの優先度を設定
        thread1.setPriority(Thread.MIN_PRIORITY); // 最低優先度
        thread2.setPriority(Thread.MAX_PRIORITY); // 最高優先度
        
        thread1.start();
        thread2.start();
    }
}
  • スレッドの開始にはstart()メソッドを使用する。
  • スレッドを一時停止させるにはsleep()メソッドを使用する。
  • スレッドを停止させるにはinterrupt()メソッドを使用する。
  • スレッドの優先度はsetPriority()メソッドで設定できる。

次のセクションでは、スレッドの同期と競合の回避について解説します。

スレッドの同期と競合の回避

スレッドを使用する際には、複数のスレッドが同じリソースにアクセスすることによる競合状態を避けるために、スレッドの同期が重要です。

競合状態が発生すると、データの不整合や予期しない動作が引き起こされる可能性があります。

ここでは、Javaにおけるスレッドの同期方法と競合の回避策について解説します。

同期化の必要性

複数のスレッドが同時に同じデータにアクセスし、変更を行う場合、データの整合性を保つために同期が必要です。

例えば、カウンターの値をインクリメントする処理を考えます。

以下のようなコードでは、競合状態が発生する可能性があります。

// App.java
public class App extends Thread {
    private static int counter = 0; // 共有リソース
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            counter++; // 競合状態が発生する可能性がある
        }
    }
    public static void main(String[] args) throws InterruptedException {
        App thread1 = new App();
        App thread2 = new App();
        
        thread1.start();
        thread2.start();
        
        thread1.join();
        thread2.join();
        
        System.out.println("最終カウンターの値: " + counter); // 期待される値は2000
    }
}

このコードでは、counterが2つのスレッドから同時にアクセスされるため、最終的な値が期待通りにならないことがあります。

同期化の方法

synchronizedキーワード

Javaでは、synchronizedキーワードを使用してメソッドやブロックを同期化することができます。

これにより、同時に1つのスレッドだけがそのメソッドやブロックにアクセスできるようになります。

以下はそのサンプルコードです。

// App.java
public class App extends Thread {
    private static int counter = 0; // 共有リソース
    // synchronizedメソッド
    private synchronized void increment() {
        counter++; // 競合状態を回避
    }
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            increment(); // synchronizedメソッドを呼び出す
        }
    }
    public static void main(String[] args) throws InterruptedException {
        App thread1 = new App();
        App thread2 = new App();
        
        thread1.start();
        thread2.start();
        
        thread1.join();
        thread2.join();
        
        System.out.println("最終カウンターの値: " + counter); // 期待される値は2000
    }
}

このコードでは、incrementメソッドがsynchronizedとして宣言されているため、同時に1つのスレッドだけがこのメソッドを実行できます。

これにより、競合状態を回避できます。

synchronizedブロック

特定のコードブロックだけを同期化したい場合は、synchronizedブロックを使用します。

以下はそのサンプルコードです。

// App.java
public class App extends Thread {
    private static int counter = 0; // 共有リソース
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            synchronized (App.class) { // synchronizedブロック
                counter++; // 競合状態を回避
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        App thread1 = new App();
        App thread2 = new App();
        
        thread1.start();
        thread2.start();
        
        thread1.join();
        thread2.join();
        
        System.out.println("最終カウンターの値: " + counter); // 期待される値は2000
    }
}

このコードでは、synchronizedブロックを使用して、counterのインクリメント処理を同期化しています。

これにより、必要な部分だけを同期化することができます。

  • スレッドの競合状態を避けるために、同期が必要である。
  • synchronizedキーワードを使用してメソッドやブロックを同期化することができる。
  • synchronizedメソッドとsynchronizedブロックの2つの方法がある。

次のセクションでは、実践例として複数スレッドでタスクを並行処理する方法について解説します。

実践例:複数スレッドでタスクを並行処理する

ここでは、複数のスレッドを使用してタスクを並行処理する実践例を紹介します。

この例では、複数のスレッドが異なるタスクを同時に実行し、最終的な結果を集約するシンプルなプログラムを作成します。

具体的には、各スレッドが数値の合計を計算し、その結果をメインスレッドで集約します。

以下のコードは、複数のスレッドがそれぞれ異なる範囲の数値の合計を計算する例です。

// App.java
public class App extends Thread {
    private int start; // 開始値
    private int end;   // 終了値
    private int sum;   // 合計値
    // コンストラクタ
    public App(int start, int end) {
        this.start = start;
        this.end = end;
        this.sum = 0;
    }
    @Override
    public void run() {
        // 指定された範囲の合計を計算
        for (int i = start; i <= end; i++) {
            sum += i;
        }
        System.out.println("範囲 " + start + " から " + end + " の合計: " + sum);
    }
    // 合計値を取得するメソッド
    public int getSum() {
        return sum;
    }
    public static void main(String[] args) throws InterruptedException {
        // スレッドのインスタンスを作成
        App thread1 = new App(1, 50);   // 1から50の合計
        App thread2 = new App(51, 100);  // 51から100の合計
        
        // スレッドを開始
        thread1.start();
        thread2.start();
        
        // スレッドの終了を待機
        thread1.join();
        thread2.join();
        
        // 合計値を集約
        int totalSum = thread1.getSum() + thread2.getSum();
        System.out.println("最終合計: " + totalSum); // 期待される値は5050
    }
}
  1. クラスの定義: AppクラスはThreadを継承し、各スレッドが計算する範囲を指定するためのstartendのフィールドを持っています。

また、計算結果を格納するためのsumフィールドもあります。

  1. コンストラクタ: コンストラクタで開始値と終了値を受け取り、合計値を初期化します。
  2. runメソッド: runメソッド内で、指定された範囲の合計を計算します。

計算が完了すると、結果をコンソールに出力します。

  1. 合計値の取得: getSumメソッドを使用して、計算した合計値をメインスレッドで取得します。
  2. メインメソッド: メインメソッドでは、2つのスレッドを作成し、それぞれ異なる範囲の合計を計算します。

joinメソッドを使用して、両方のスレッドが終了するのを待機し、最終的な合計を計算して出力します。

このプログラムを実行すると、以下のような出力が得られます。

範囲 1 から 50 の合計: 1275
範囲 51 から 100 の合計: 3775
最終合計: 5050
  • 複数のスレッドを使用して、異なるタスクを並行処理することができる。
  • 各スレッドが独立して計算を行い、最終的な結果をメインスレッドで集約することが可能である。

次のセクションでは、スレッドを使う際のベストプラクティスについて解説します。

スレッドを使う際のベストプラクティス

スレッドを使用する際には、パフォーマンスや安全性を考慮したベストプラクティスを守ることが重要です。

以下に、Javaでスレッドを効果的に利用するためのポイントをいくつか紹介します。

スレッドの数を適切に設定する

  • スレッドの数は、システムのリソースや実行するタスクの性質に応じて適切に設定する必要があります。
  • 一般的には、CPUコア数に基づいてスレッド数を決定することが推奨されます。

例えば、CPUコア数が4の場合、スレッド数を4またはそれ以下に設定するのが理想です。

スレッドプールを利用する

  • スレッドプールを使用することで、スレッドの生成と破棄にかかるオーバーヘッドを削減できます。
  • Javaでは、ExecutorServiceを使用してスレッドプールを簡単に管理できます。

以下はそのサンプルコードです。

// App.java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class App implements Runnable {
    @Override
    public void run() {
        System.out.println("スレッドが実行中: " + Thread.currentThread().getName());
    }
    public static void main(String[] args) {
        // スレッドプールを作成
        ExecutorService executor = Executors.newFixedThreadPool(3);
        
        // タスクをスレッドプールに送信
        for (int i = 0; i < 5; i++) {
            executor.submit(new App());
        }
        
        // スレッドプールをシャットダウン
        executor.shutdown();
    }
}

適切な同期を行う

  • 共有リソースにアクセスする際は、必ず適切な同期を行い、競合状態を回避することが重要です。
  • synchronizedキーワードやReentrantLockを使用して、必要な部分だけを同期化するように心がけましょう。

デッドロックを避ける

  • デッドロックは、複数のスレッドが互いにリソースを待ち続ける状態です。

これを避けるためには、リソースの取得順序を統一することが重要です。

  • 可能であれば、タイムアウトを設定して、リソースの取得に失敗した場合にスレッドが待機し続けないようにします。

スレッドの例外処理

  • スレッド内で発生した例外は、メインスレッドに影響を与える可能性があります。

スレッド内で適切に例外処理を行い、エラーログを記録することが重要です。

// App.java
public class App extends Thread {
    @Override
    public void run() {
        try {
            // 例外を発生させる処理
            throw new RuntimeException("エラーが発生しました。");
        } catch (Exception e) {
            System.err.println("スレッド内で例外が発生: " + e.getMessage());
        }
    }
    public static void main(String[] args) {
        App thread = new App();
        thread.start();
    }
}

スレッドの状態を監視する

  • スレッドの状態を監視することで、パフォーマンスのボトルネックや問題を早期に発見できます。
  • スレッドの状態を確認するために、Threadクラスのメソッド(getState()など)を活用しましょう。
  • スレッドの数を適切に設定し、スレッドプールを利用することでパフォーマンスを向上させる。
  • 適切な同期を行い、デッドロックを避けることが重要である。
  • スレッド内での例外処理や状態監視を行い、安定したアプリケーションを実現する。

これらのベストプラクティスを守ることで、Javaにおけるスレッドの利用がより効果的かつ安全になります。

まとめ

この記事では、Javaにおけるスレッドの基本的な概念から、スレッドの作成方法、制御方法、同期と競合の回避、さらには実践例を通じて複数スレッドでのタスクの並行処理について詳しく解説しました。

また、スレッドを使用する際のベストプラクティスについても触れ、効率的かつ安全にスレッドを活用するためのポイントを紹介しました。

これらの知識を活かして、実際のプログラムにスレッドを取り入れ、パフォーマンスの向上を図ってみてください。

関連記事

Back to top button