mutao.net

いわゆる雑記。

Java並列処理実装について

軽くJavaの並列処理の実装について調べてみたのでメモしておく。

Javaのスレッド

Javaのスレッドはカーネルスレッドと結びついている。

基本的には1coreあたり1スレッドしか動作できないためスレッドを切り替えて動作することになる。

このスレッドの切り替えのことをコンテキストスイッチという。

コンテキストスイッチ

コンテキストスイッチを行うためにはスレッドが動いていた状態を再開するために保存しておく。

つまりメモリに状態を保存しておくことになる。

また、コンテキストスイッチには約1~100マイクロ秒かかるこの時間にはスレッドの処理時間は含まれていない。

並列処理をしてもコンテキストスイッチが頻繁に起こるようになればここがボトルネックとなり並列処理を実装してもアプリケーションの性能劣化を招く。

タスクの並列度はコア数を前提として考えなければならない。

スタックサイズ

64bit環境のJVMのデフォルトのスタックサイズは1MB。

つまり、1k個のスレッドは1GBのRAMを消費することになる。

Javaの並列処理実装へのアプローチ

実際にwhile loopの中で大量にThreadを作成するテストコードを書いてみるとOutOfMemoryエラーが発生した。

これはThreadの作成に失敗したことを意味しているらしい。

実際に仕事で並列処理を使うことは多そうなのでこのようなエラーに遭遇しないためにも適切なアプローチを学びたい。

java.lang.Thread

docs.oracle.com

Java1.0からある老舗のクラス。

Javaの並列処理は全てこのクラスがベースになっている。

public class ExcecuteThread extends Thread{
    public void run() {
        // 実行したい処理
        }
    }
}
// こんな感じで実行
public class Main {
    public static void main(String[] args) {
      ExcecuteThread thread = new ExcecuteThread();
      thread.start();
    }
}

このようにスレッドとタスクは1対1の関係性を伴う。

Threadクラスはどこでも作成でき、どこからでも実行することが可能だが、Threadの実行タイミングや現在実行されているスレッド数の管理が大変。

なのでThreadクラスを生で使うのではなくてAPIを使用して実装した方がよいとのこと

Parallel Stream

Streamの並列処理版というらしい。Java8から導入。

public class Main {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        int num = 0;
        while(num < 1000) {
            list.add(num);
            num++;
        }
        list.parallelStream().forEach(i -> System.out.println(i));
    }
}

面白くも何もないコードですが、こんな感じでテストコードを書いてみました。

実行結果は並列処理なので順序はめちゃくちゃです。

streamをparallelStreamにすればいいだけなので簡単な実装ですが、実際には使い所がある模様。

並列処理をする要素数が多ければパフォーマンスの向上が見込めるが、逆に少ないとパフォーマンスとしては逐次実行より劣化するとのこと。

これはテストコードでいうところのlist変数の中の要素を分割してから並列処理と終端操作を行うためとのこと。

要素を分割するということは要素数が決まっていないlistなどに対しては要素の分割操作ができないためparallelStreamは使えないということ。

parallelStreamについては以下にベンチマーク結果があるみたいです。

github.com

参考にさせていただいた記事

rcoh.me

mahata.gitlab.io