Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Java並行処理の基本/Java Concurrency

Java並行処理の基本/Java Concurrency

Java初心者向けの勉強会用資料

Toshiyuki Imaizumi

May 29, 2020
Tweet

More Decks by Toshiyuki Imaizumi

Other Decks in Programming

Transcript

  1. 目次 • 並行処理とは • プロセスとスレッド • Javaで並行処理を行う • Threadを使った実装 •

    並行処理を行う際の注意点 • スレッドセーフ • synchronized • スレッドセーフなクラス • デッドロック • 実践的なマルチスレッドクラス • Executor Framework、Fork/Join 2
  2. 並行処理とは • 複数の処理を論理的に同時に実行すること • 物理的に同時に実行できる処理数は限りがある(CPUのコア数に依存) • 複数の処理を高速で切り替えて実行し、同時にやっているように見える • 複数の処理を物理的に同時に行うことは「並列処理」と呼ばれる •

    並行処理を実現するために「プロセス」と「スレッド」という概念がある • プロセス • OSから見える処理の単位 • 他のプロセスとメモリ空間を共有しない • スレッド • プロセスの中で並行処理を行う仕組み • 他のスレッドとメモリ空間を共有する 3
  3. • ローカル変数は各スレッド毎に保持されるが、それ以外の値は共有 • 複数のスレッドから同じ変数の値を書きかえる場合は注意が必要(後述) perspective=java workspace= C:¥workspace … プロセス、スレッドのイメージ図(1/2) プロセス(例:Eclipse)

    メモリ スレッドA(ビルド) src=/src jdk=11 スレッドB(静的解析1) src=hoge.java lineNum=100 complexity=5 スレッドD(描画) fps=60 スレッドC(静的解析2) src=piyo.java lineNum=20 complexity=1 4
  4. HOGE=hoge プロセス、スレッドのイメージ図(2/2) メモリ サンプルクラス public class ClassA{ static String HOGE

    = “hoge”; private int count; public void add(int n){ int adder = n * n; count += adder; } } クラス変数 インスタンス count=0 count=1 n adder メソッド スレッドA n=2 adder=4 スレッドB n=3 adder=9 5
  5. Javaとマルチスレッド • プログラム開始時はmainメソッドを実行するスレッドが一つのみ存在 する • Threadクラスを使うことで新たにスレッドを作成し、処理を並行して行 える(マルチスレッド) • Threadを使って並行処理を行うためには以下のどちらかの方法で行 う

    • Threadクラスを継承し、runメソッドをオーバーライドして処理を記述 • Runnableインタフェースを実装したクラスを作成し、runメソッドに処理を記述。 Threadのコンストラクタ引数に作成したクラスを渡す • 後者の例を示す • 前者は継承が使えなくなってしまうのでよくない 6
  6. Threadを利用する(1/2) • 実行例 public class Outputter implements Runnable { private

    String name; public Outputter(String name) { this.name = name; } @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println(name + ":" + i); } } } スレッドで実行されるクラス public static void main(String[] args) { Thread thread1 = new Thread(new Outputter("thread1")); Thread thread2 = new Thread(new Outputter("thread2")); thread1.start(); thread2.start(); System.out.println("main thread end"); } 並行処理を開始するメソッド行うクラス 7
  7. スレッドセーフでない例1・インクリメント • 複数のスレッドで同一の数値をインクリメントする例 public class Holder { private int count;

    public void add() { count++; } public int getCount(){ return count; } } スレッドセーフでないクラス public class Counter implements Runnable { private Holder holder; public Counter(Holder holder) { this.holder = holder; } @Override public void run() { for (int i = 0; i < 100000; i++) { holder.add(); } } } スレッドで実行されるクラス public static void main(String[] args) throws InterruptedException { Holder holder = new Holder(); Thread thread1 = new Thread(new Counter(holder)); Thread thread2 = new Thread(new Counter(holder)); thread1.start(); thread2.start(); //スレッドの処理が終わるまで1秒待つ Thread.sleep(1000); // 100000*2で200000となるはずだが… System.out.println(holder.getCount()); } スレッドを実行するメソッド 11
  8. スレッドセーフでない例1・インクリメント • count++はマルチスレッドで以下のような順序で実行されると値が加 算されない • 1. スレッドAでcountの値を一時領域に保持(count=0,一時領域=0) • 2. スレッドBでcountの値を一時領域に保持(count=0,一時領域=0)

    • 3. スレッドAで一時領域の値に1を足す(count=0,一時領域=1) • 4. スレッドBで一時領域の値に1を足す(count=0,一時領域=1) • 5. スレッドAで変数countに一時領域の値を戻す(count=1,一時領域=1) • 6. スレッドBで変数countに一時領域の値を戻す(count=1,一時領域=1) • この例をスレッドセーフにするために、以下の二つの対応案がある • スレッドA,Bで同時にcount++が呼ばれないように制御する • スレッドセーフなクラスを利用してcountの値を保持する 13
  9. synchronizedの挙動 • ロックがかかるのはインスタンスに対してのため、以下のようにイン スタンスが異なれば複数のスレッドで同時にHolder#addを呼び出せ る public static void main(String[] args)

    throws InterruptedException { Holder holder1 = new Holder(); Holder holder2 = new Holder(); Thread thread1 = new Thread(new Counter(holder1)); Thread thread2 = new Thread(new Counter(holder2)); thread1.start(); thread2.start(); } スレッドを実行するメソッド 15
  10. synchronizedの挙動 • ロックがかかるのはインスタンスに対してのため、以下のように別メ ソッドであってもロックの取得待ちが起きる スレッドから参照されるクラス public class Holder { private

    int count; public synchronized void add() { count++; } public synchronized void sub() { count--; } } public class Counter1 implements Runnable { @Override public void run() { holder.add(); } } スレッドで実行されるクラス public class Counter2 implements Runnable { @Override public void run() { holder.sub(); } } スレッドA holder.add()を呼び出し スレッドB holder.sub()を呼び出し holderのロックを取得 holderのロックはスレッドAに取られているため待ち addメソッド終了 subメソッド開始 16
  11. synchronizedの挙動 • synchronizedは再入性(reentrant)があるため、ロックをかけたスレッ ドと同じスレッドであれば、synchronizedな処理を再度呼び出せる public synchronized void add(int n){ if(n

    < 100){ // 再入性がないと、ここで処理待ちになってしまう add(n+1); } else{ count += n; } } 例:同一スレッドでsynchronizedなメソッドを複数回呼ぶ例 スレッドA holder.add(5) を呼び出し holderのロック を取得 holder.add(6) を呼び出し 既にholderのロックを持ってい るため、add(6)の処理実行 holder.add(5) の処理実行 17
  12. synchronizedの挙動 • synchronizedはメソッド単位ではなく、メソッドの中の処理単位でも書 くことができる • スレッドセーフな他の処理がある場合に効率的にロックを取れる public class Holder {

    private int count; public void add() { int added = calcCount(); // スレッドセーフな重い処理 // 値の変更箇所のみHolderのロックを取ってスレッドセーフに synchronized(this){ count += added: } } } メソッド単位ではなく処理単位にロックを取る例 18
  13. スレッドセーフでない例2・ArrayList#add • 複数のスレッドで同一のArrayListに要素を追加する例 public class Holder { private List<Integer> list;

    public void add(Integer i) { list.add(i); } public List<Integer> getList(){ return list; } } スレッドセーフでないクラス public class Counter implements Runnable { private Holder holder; public Counter(Holder holder) { this.holder = holder; } @Override public void run() { for (int i = 0; i < 100000; i++) { holder.add(i); } System.out.println(“endAdd”); } } スレッドで実行されるクラス public static void main(String[] args) throws InterruptedException { Holder holder = new Holder(); Thread thread1 = new Thread(new Counter(holder)); Thread thread2 = new Thread(new Counter(holder)); thread1.start(); thread2.start(); //スレッドの処理が終わるまで1秒待つ Thread.sleep(1000); // 100000*2で200000となるはずだが… System.out.println(holder.getList().size()); } スレッドを実行するメソッド 20
  14. スレッドセーフでない例2・ArrayList#add • さきほどのクラスを実行すると、実行する度にリストのsizeが変わる • また、まれにArrayIndexOutOfBoundsExceptionが発生する • 該当部のコードは以下(※JDK8の実装) Exception in thread

    "Thread-1" java.lang.ArrayIndexOutOfBoundsException: 823 at java.util.ArrayList.add(ArrayList.java:463) at jp.gr.java_conf.tsyki.thread.lecture.ThreadListAddSample$CountHolder.add(ThreadListAddSample.java:16) at jp.gr.java_conf.tsyki.thread.lecture.ThreadListAddSample$Counter.run(ThreadListAddSample.java:35) at java.lang.Thread.run(Thread.java:748) public boolean add(E e) { this.ensureCapacityInternal(this.size + 1); //要素を保持している配列が足りなくなったら増やす処理 this.elementData[this.size++] = e; //要素を保持している配列に値を設定 return true; } 21
  15. 不具合が起きるケース • ArrayListのelementDataの空きが1つだけの状態、 (例:elementData.length=16、size=15) かつ以下の順で実行されるとArrayIndexOutOfBoundsExceptionが発 生する • スレッドAでensureCapacityするがsize+1の領域は空いているため何もしない • スレッドBでensureCapacityするがsize+1の領域は空いているため何もしない

    • スレッドAでsizeをインクリメントし、sizeの位置に値設定 (length=16,size=16) • スレッドBでsizeをインクリメントし、sizeの位置に値設定時、配列の長さが足り ないので例外発生(length=16,size=17) • この例でもsynchronizedを使った制御と、スレッドセーフなクラスの利 用のどちらでも対応ができる 22
  16. スレッドセーフなクラスを使う • コレクションフレームワークにはスレッドセーフな実装が用意されて いる • java.util.concurrentパッケージに存在 • CopyOnWriteArrayList • スレッドセーフな代わりに要素の追加は低速

    • ConcurrentHashMap • Collections#synchronizedListでラップする事でも既存のリストをスレッ ドセーフにできる • この場合はイテレートは同期化されないことに注意(次のスライド参照) 23
  17. スレッドセーフでない例3・ArrayList#iterator • 複数のスレッドで同一のArrayListに要素を追加、参照する例 @Override public void run() { for (int

    i = 0; i < 100000; i++) { holder.add(i); for(Integer value : holder.getList()){ System.out.println(value); } } } スレッドで実行されるクラス public class Holder { private List<Integer> list; public synchronized void add(Integer i) { list.add(i); } public List<Integer> getList(){ return list; } } addをスレッドセーフにしたクラス Exception in thread "Thread-1" java.util.ConcurrentModificationException at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909) at java.util.ArrayList$Itr.next(ArrayList.java:859) at jp.gr.java_conf.tsyki.thread.lecture.ThreadListIteratorSample$Counter.run(ThreadListIteratorSample.java:40) at java.lang.Thread.run(Thread.java:748) 24
  18. スレッドセーフでない例3・ArrayList#iterator • 例外となった箇所のArrayListのコードを見てみると… • modCountはリストが追加、削除されたときに加算される変更回数のこと • ループを開始してから、対象のリストに要素が追加、削除された場合、 ConcurrentModificationExceptionが発生するようになっている • 別スレッドからaddされたのでこのエラーになった

    • CopyOnWriteArrayListを使うか、ループ前にリストを作り直す private class Itr implements Iterator<E> { int expectedModCount = modCount; public E next() { checkForComodification(); ... } } final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } 26
  19. ループ前にリストを作り直して対応する例 @Override public void run() { for (int i =

    0; i < 100000; i++) { holder.add(i); for(Integer value : new ArrayList<>(holder.getList())){ System.out.println(value); } } } スレッドで実行されるクラス 27
  20. デッドロック • スレッドAでappend(list1,list2)を、 スレッドBでappend(list2,list1)を呼ぶとデッドロック スレッドA list1のロックを 取得 synchronized (srcList) スレッドB

    synchronized (destList) list2のロックを 取得 list2のロックを取得しようとするが、 スレッドBが取得しているため待機 list1のロックを取得しようとするが、 スレッドAが取得しているため待機 デッドロック 発生 30
  21. デッドロックを回避する • ロックを取る順番を同一にするよう書き換える public static void append(List<Integer> srcList, List<Integer> destList)

    { // ロックを取る順序を決めるための値を計算 int srcHash = System.identityHashCode(srcList); int destHash = System.identityHashCode(destList); if(srcHash < toHash){ synchronized (srcList) { for (Integer srcValue : srcList) { synchronized (destList) { destList.add(srcValue); } } } } ... else if(srcHash > toHash){ synchronized (destList) { synchronized (srcList) { for (Integer srcValue : srcList) { destList.add(srcValue); } } } } } else{ // どちらが先か判断できない場合は // あらかじめ用意しておいたロック用インスタンスを使う synchronized (tieLock) { synchronized (srcList) { ... 31
  22. デッドロックにならなくなった例 • スレッドAでappend(list1,list2)を、 スレッドBでappend(list2,list1)を呼ぶ(hashCodeはlist1<list2とする) スレッドA list1のロックを 取得 srcHash<destHashのため、 synchronized (srcList)

    スレッドB synchronized (destList) list1のロックを 取得しようとし て待ち list2のロックを取得 list1のロックを 取得して続行 srcHash>destHashのため、 synchronized (destList) メソッド終了 32
  23. デッドロックを検出する • JDK同梱のjstackを使うことでデッドロックが生じている場所を見れる • コマンドプロンプトで「jstack {対象のjavaプロセス番号}」を実行 • 以下のような結果が取得できる "Thread-1" #12

    prio=5 os_prio=0 tid=0x000000001f6a6800 nid=0x3ac0 waiting for monitor entry [0x000000002035f000] java.lang.Thread.State: BLOCKED (on object monitor) at jp.gr.java_conf.tsyki.thread.lecture.ThreadDeadLockSample.append(ThreadDeadLockSample.java:64) - waiting to lock <0x000000076b0e0b80> (a java.util.ArrayList) - locked <0x000000076b0e0cf0> (a java.util.ArrayList) at jp.gr.java_conf.tsyki.thread.lecture.ThreadDeadLockSample$SampleThread.run(ThreadDeadLockSample.java:50) "Thread-0" #11 prio=5 os_prio=0 tid=0x000000001f5a0000 nid=0x4e40 waiting for monitor entry [0x000000002025f000] java.lang.Thread.State: BLOCKED (on object monitor) at jp.gr.java_conf.tsyki.thread.lecture.ThreadDeadLockSample.append(ThreadDeadLockSample.java:64) - waiting to lock <0x000000076b0e0cf0> (a java.util.ArrayList) - locked <0x000000076b0e0b80> (a java.util.ArrayList) at jp.gr.java_conf.tsyki.thread.lecture.ThreadDeadLockSample$SampleThread.run(ThreadDeadLockSample.java:50) 33
  24. Executor Frameworkを利用する • Threadはシンプルなクラスであり使いづらい • 例えば、複数スレッドで計算を行わせ、各スレッドの処理が終わるまで待っ てから結果を集計する、といったことがやりづらい • CountDownLatchなどスレッド間で同期をとるためのクラスは用意はされている •

    マルチスレッドを扱う場合はExecutor Frameworkを利用するとよい • スレッドの結果を受け取るためのインタフェースが存在する • 使い終わったスレッドを再利用するためのスレッドプールという仕組みがあ る • Threadの代わりにExecutorSeviceを使ってスレッドを実行させる • Runnable#runの代わりにCallable#callを実装して処理を行う 35
  25. Executor Frameworkを利用する • Executor Frameworkは以下のように使う • 1. スレッドプールを用意する • Executorsのstaticメソッドを使ってExecutorServiceを作成

    • 2. スレッドを開始する • ExecutorService#submitにCallableを実装したクラスを渡してスレッドを開始 • スレッドの処理を待つ場合や結果を取得したい場合、返り値のFutureを保持しておく • 3. 必要ならスレッドの処理が終わるのを待つ • Future#getを呼びだす • 4. スレッドプールに終了を通知する • ExecutorService#shutdownを呼び出す • これをやらないとスレッドプールが保持するスレッドが実行中のままになり、プログラム が終了とならない • スレッドプールによっては一定時間後に自動で終了される 36
  26. Executor Frameworkを利用する(返り値なし) public class OutputterExecutor implements Callable<Void> { private String

    name; public OutputterExecutor(String name) { this.name = name; } @Override public Void call() throws Exception { for (int i = 0; i < 100; i++) { System.out.println(name + ":" + i); } return null; } } スレッドで実行されるクラス public static void main(String[] args) { // スレッドプールを作成 ExecutorService executor = Executors.newCachedThreadPool(); Callable<Void> call1 = new OutputterExecutor("1"); Callable<Void> call2 = new OutputterExecutor("2"); // スレッドを開始 Future<Void> future1 = executor.submit(call1); Future<Void> future2 = executor.submit(call2); try { future1.get(); //スレッドの処理が終わるまで待つ future2.get(); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } System.out.println("main thread end"); executor.shutdown(); //スレッドプールに終了を通知 } 並行処理を開始するメソッド行うクラス 37
  27. Executor Frameworkを利用する(返り値あり) public class OutputterExecutor implements Callable<Integer> { private String

    name; public OutputterExecutor(String name) { this.name = name; } @Override public Integer call() throws Exception { int sum = 0; for (int i = 0; i < 100; i++) { sum += i; } return sum; } } スレッドで実行されるクラス public static void main(String[] args) { ExecutorService executor = Executors.newCachedThreadPool(); Callable<Integer> call1 = new OutputterExecutor("1"); Callable<Integer> call2 = new OutputterExecutor("2"); Future<Integer> future1 = executor.submit(call1); Future<Integer> future2 = executor.submit(call2); int sum = 0; try { //スレッドの処理が終わるまで待って計算結果を受け取る sum += future1.get(); sum += future2.get(); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } System.out.println("main thread end"); executor.shutdown(); } 並行処理を開始するメソッド行うクラス 38
  28. スレッドプールについて • Executors.newCachedThreadPool • 必要に応じてスレッドを作成する • スレッドの処理完了後、60秒後にそのスレッドは破棄される。60秒以内であ ればスレッドは再利用されるため効率がよい • スレッドが多くなりすぎるとCPUがスレッドを切り替える処理(コンテキストス

    イッチ) のコストが大きくなり、パフォーマンスが落ちるため注意 • Executors.newFixedThreadPool(nThreads) • 引数で指定されたスレッド数を上限として実行する • スレッドの数が多くなりすぎないようにできる • DBへの同時アクセス数が制限されている、というような場合にも利用できる • 実行中のスレッドが全て他のスレッドの結果を待っているような状態の場合、 デッドロックが発生することに注意 39
  29. Fork/Join Frameworkを利用する • JDK1.7から追加された、特定状況下で効率よくスレッドを使うための 仕組み • 処理を(再帰的に)分割して実行するのに適している。 • 「処理(データ)を分割してそれぞれを子スレッドで並列実行し、それが終わ ると自分自身のスレッドも終了」という処理が向いている。

    • Fork/Joinフレームワークでは他のスレッドの処理を待っているスレッドを使い まわすという特徴がある • このような処理を通常のスレッドプールを使ってやると、スレッド数が膨大に なってしまい効率が悪い • 例:指定のフォルダ以下のファイルをカウントする 42
  30. 再帰的にファイル数を取得する例 root dir1 dir2 dir3 dir1-1 dir1-2 ①スレッド1でroot以下のフォルダを探索。 フォルダ毎にそのフォルダ内を探索するスレッドを作成する (dir1,2,3で3スレッド追加)。各スレッドの結果が出るまで待ち

    ②スレッド2でdir1以下の フォルダを探索(dir1- 1,dir1-2で2スレッド追加)。 各スレッドの結果が出る まで待ち • 通常だと最大でフォルダ数分のスレッドが必要になってしまう • スレッドプールが固定数だと、スレッドが足りなくなってデッドロックする • Fork/Joinを使うと、処理待ちのスレッドを使いまわすため、スレッド数 が減る。上記の例だと、dir1-1の探索時にスレッド1が使われうる 43
  31. Fork/Join Frameworkを利用する • Fork/Joinは以下のように使う • 1. スレッドプールを用意する • ForkJoinPoolをnewする •

    2. スレッドを開始する • ForkJoinPool#submitにForkJoinTaskを継承したクラスを渡してスレッドを開始 • 返り値でForkJoinTaskが返るが、これは引数で指定したものと同じなので不要 • 3. 必要ならスレッドの処理が終わるのを待つ • ForkJoinTask#joinを呼びだす • 実装例は以下を参照 • https://github.com/tsyki/concurrent- example/blob/master/src/jp/gr/java_conf/tsyki/thread/lecture/ForkJoinFileCounter.java • 今回のスライドに記載のサンプルコードも上記githubにあり 44
  32. まとめ • Threadクラスを利用することで並行処理ができる • 並行処理で同じデータを書き換える場合は不整合がおきないように する必要がある(スレッドセーフ) • synchronizedを使うことでロックを取得する • スレッドセーフなクラスを使う

    • synchronizedを使う場合はデッドロックが発生しないようにする • 1スレッドで複数のロックを取る場合、常に同じ順番でロックを取る • Threadよりも実践的なクラスとしてExecutor Frameworkがある • 並行処理の結果を待って、計算結果を取得するインタフェースがある • 再帰的にスレッドを作成する場合はFork/Joinを使うと効果的 45