Javaのマルチスレッド処理【プログラミング初心者向け教材】

プログラミング
スポンサーリンク

たくさんの処理を効率よく行うための手法として並列処理があります。

処理の単位をスレッドと呼び、並列で処理することをマルチスレッドと呼びます。

Javaではマルチスレッド処理を行うための機能が標準で提供されています。

本記事では、Javaのマルチスレッド処理について解説します。

プログラミング初心者の方の学習や、忘れてしまった方の復習として、参考にしていただければ幸いです。

記載しているプログラムは、Java11を使って動作確認をしています。

Javaのマルチスレッド処理

Javaのマルチスレッド処理について、以下の内容で解説します。

  • マルチスレッドを扱うクラス・パッケージ
  • スレッドの作成と起動
  • スレッドプールの作成と起動
  • スレッドからの戻り値の取得

マルチスレッドを扱うクラス・パッケージ

Javaでスレッドを扱うには、java.lang.Threadクラスやjava.lang.Runnableインタフェースを使用します。

また、スレッドを複数まとめて効率よく扱うスレッドプールを利用するには、java.util.concurrentパッケージの各クラスを使います。

スレッドの作成と起動

Javaでスレッドを作成するには、Threadクラスを継承する方法とRunnableインタフェースを実装する2つの方法があります。

Threadクラスの継承する

Threadクラスを継承する場合は、runメソッドをオーバーライドして実装する必要があります。

class ThreadA extends Thread {

    private String name;
    public ThreadA(String name) {
        super();
        this.name = name;
    }

    @Override
    public void run() {
        System.out.println(String.format("%s start.", this.name));
        try {
            Thread.sleep(new Random().nextInt(3000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(String.format("%s end.", this.name));
    }
}

上記のプログラムでは、runメソッドを実装し、3秒以内のランダムな時間スリープする処理を実行します。
このスレッドを作成して実行するには、このクラスを作成して start を呼出します。

ThreadA t1 = new ThreadA("スレッドA-1");
ThreadA t2 = new ThreadA("スレッドA-2");
t1.start();
t2.start();
t1.join();
t2.join();

joinは、スレッドの処理が完了するまで待つ処理になります。
実行結果は以下のようになります。

スレッドA-2 start.
スレッドA-1 start.
スレッドA-1 end.
スレッドA-2 end.

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

Runnableインタフェースを実装する場合も、runメソッドをオーバーライドして実装します。

class ThreadB implements Runnable {

    private String name;
    public ThreadB(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        System.out.println(String.format("%s start.", this.name));
        try {
            Thread.sleep(new Random().nextInt(3000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(String.format("%s end.", this.name));
    }
}

上記のプログラムも先程と同様に、3秒以内のランダムな時間スリープする処理を実行します。
このスレッドを作成して実行するには、このクラスを作成し、Threadクラスでラップして start を呼出します。

ThreadB r1 = new ThreadB("スレッドB-1");
ThreadB r2 = new ThreadB("スレッドB-2");
Thread t3 = new Thread(r1);
Thread t4 = new Thread(r2);
t3.start();
t4.start();
t3.join();
t4.join();

実行結果は以下のようになります。

スレッドB-1 start.
スレッドB-2 start.
スレッドB-2 end.
スレッドB-1 end.

Runnableインタフェースを実装した場合も、動作上はThreadクラスを継承する方法と変わりはありません。

Threadクラスを継承した場合は、他のクラスを継承することができませんが、Runnableインタフェースの実装であれば、他のクラスを継承することができるメリットがあります。

スレッドプールの作成と起動

スレッドはいくつでも作って並列で処理させることが可能ですが、数が多すぎると逆に効率が悪くなることがあります。
スレッドが処理する数を制限する際に用いるのが、スレッドプールです。

スレッドプールは、スレッドの登録はいくつでも出来ますが、処理する最大数は決められた数までになります。
最大数は基本的に処理するマシンのCPUの数にすることが多いです。

スレッドプールの作成には、Executorsクラスを利用します。
スレッドプールの種類はいくつかあるのですが、今回は最大実行スレッド数を固定値にする方法で作成します。

ExecutorService es1 = Executors.newFixedThreadPool(3);
for (int i = 1; i <= 5; i++) {
    es1.submit(new ThreadB(String.format("スレッドC-%d", i)));
}
es1.shutdown();
es1.awaitTermination(10000, TimeUnit.SECONDS);

上記のプログラムでは、スレッド実行数を3に設定しています。
スレッドの起動は submit を呼出します。
その際に、先程のRunnableインタフェースを実装したクラスを指定しています。

スレッドを起動した後に、shutdownによってこれ以上のスレッドを登録が出来ないようにしています。
また、awaitTerminationによって、スレッドの処理が完了するまで待っています。

5スレッドをループで作成して実行した結果は以下のようになります。

スレッドC-1 start.
スレッドC-2 start.
スレッドC-3 start.
スレッドC-3 end.
スレッドC-4 start.
スレッドC-1 end.
スレッドC-5 start.
スレッドC-4 end.
スレッドC-5 end.
スレッドC-2 end.

上記の結果では、スレッド1、2、3が最初に起動し、スレッド4がスレッド3の終了後まで待っていることがわかります。
スレッド1の終了後に、スレッド5が開始されていて、最大実行数が3になっていることが分かります。

スレッドからの戻り値の取得

スレッド内で処理した結果を受け取る場合は、submitの戻り値をFutureとして受け取ります。
受け取ったFutureは、getによって取得する事が可能です。

List<Future<String>> futures = new ArrayList<Future<String>>();
ExecutorService es2 = Executors.newFixedThreadPool(3);
for (int i = 1; i <= 5; i++) {
    Future<String> f = es2.submit(new ThreadD(String.format("スレッドD-%d", i)));
    futures.add(f);
}
for (Future<String> f : futures) {
    System.out.println(f.get());
}
es2.shutdown();
es2.awaitTermination(10000, TimeUnit.SECONDS);

5つのスレッドの結果をリストに保持しておき、最後にまとめてgetを呼出しています。
先にスレッドの実行をしているので、戻り値を受け取るのに待ち時間を最小限にすることができます。

戻り値を受け取るためには、Runnableインタフェースを実装するのではなく、Callableインタフェースを実装します。
戻り値となる型をジェネリクスに指定します。

class ThreadD implements Callable<String> {

    private String name;
    public ThreadD(String name) {
        this.name = name;
    }

    @Override
    public String call() throws Exception {
        System.out.println(String.format("%s start.", this.name));
        int sleepTime = new Random().nextInt(3000);
        try {
            Thread.sleep(sleepTime);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(String.format("%s end.", this.name));
        return String.format("%s return %d", this.name, sleepTime);
    }
}

実行結果は以下のようになります。
処理が終わったスレッドから戻り値を取得できていることが分かります。

スレッドD-1 start.
スレッドD-2 start.
スレッドD-3 start.
スレッドD-3 end.
スレッドD-4 start.
スレッドD-2 end.
スレッドD-5 start.
スレッドD-1 end.
スレッドD-1 return 2767
スレッドD-2 return 1362
スレッドD-3 return 1215
スレッドD-4 end.
スレッドD-4 return 1908
スレッドD-5 end.
スレッドD-5 return 2197

時間のかかる処理はマルチスレッドで効率よく実行しよう

UI内での処理や連携するAPIでの処理は、待ち時間が長くなると、利用者に不便な印象を与えてしまいます。

時間がかかる処理はマルチスレッドで実施できないか検討し、効率よく処理を実行すると利用者の印象も良くなります。

 

今回はJavaのマルチスレッド処理を解説しました。

以上、参考になれば幸いです。

コメント

タイトルとURLをコピーしました