社会人のメモ帳

忘れたくないことアレコレ

【第3週】第3章:並列処理でエラーを解消してみる【Java Gold合格へ向けて】

PREV | LIST | NEXT

エラーを解消してみる

並列処理の実装で発生しうるエラーについて、。

  • スレッドの無駄遣い……無駄なスレッドの生成
  • 競合…複数のスレッドで1つのインスタンスを共有する
  • デッドロックインスタンスにアクセスしようとした際、別のスレッドにロックされてアクセスできなくなること

スレッドの無駄遣い

スレッドの無駄遣いを防止するため、Javaにはスレッドプールという機能が提供されている。 利用方法としてはnewCachedThreadPoolを用いてExecutorService のインスタンスを作成するだけだ。 こうして作られたインスタンスで作成されたスレッドは下記のような形で利用される。 * 60秒未満であれば再利用 * 60秒を超えると破棄される for文を用いて10個のスレッドを生成し、下記時点でスレッドを再実行した場合の状況確認する。 * 10秒時点…再利用されることが期待 * 70秒時点…破棄され新スレッドが生成されること 状況確認にはcurrentThreadメソッドを用いて、現スレッドのIDを取得することで行う。 再利用された場合は同じIDが、破棄されて新スレッドが生成された場合は新規のIDが割り振られる。

public class Main {
    public static void main(String[] args) throws Exception{
        ExecutorService exec1 = Executors.newCachedThreadPool();
        ExecutorService exec2 = Executors.newCachedThreadPool();
        Runnable test1 = () -> {
          System.out.println("exec1:"+Thread.currentThread().getId());
        };
        Runnable test2 = () -> {
            System.out.println("exec2:"+Thread.currentThread().getId());
        };

        for (int i=0;i<5;i++){
            exec1.submit(test1);
            exec2.submit(test2);
        }

        Thread.sleep(1 * 10000);
        System.out.println("---------10秒後----------");

        for (int i=0;i<5;i++){
            exec1.submit(test1);
            exec2.submit(test2);
        }

        Thread.sleep(7 * 10000);
        System.out.println("---------70秒後----------");

        for (int i=0;i<5;i++){
            exec1.submit(test1);
            exec2.submit(test2);
        }
    }
}

実行結果

exec2:14

exec2:20

exec2:22

exec1:13

exec1:17

exec2:16

exec1:15

exec2:18

exec1:19

exec1:21

---------10秒後----------

exec2:18

exec2:16

exec1:15

exec2:22

exec1:17

exec2:20

exec1:13

exec2:14

exec1:19

exec1:21

---------70秒後----------

exec1:23

exec1:23

exec2:24

exec1:23

exec2:24

exec2:25

exec2:25

exec1:23

exec2:24

exec1:26

競合

クラスTestを作成した。 変数numについて、初期値100にインクリメントするaddTestと、デクリメントするsubTestを作成した。 競合の確認とは異なり、それぞれsleepを用いてスレッドを停止する時間を設けており、指定された時間の間、それぞれのインスタンスは使用中となる。 また、値の確認をしやすいようにgetTestメソッドを追加で作成した。

class Test {
    private int num = 100;

    public int addTest(){
        try {
            Thread.sleep(1000);
        }catch (InterruptedException e){
            throw new RuntimeException(e);
        }
        return this.num++;
    }

    public int subTest(){
        try {
            Thread.sleep(1000);
        }catch (InterruptedException e){
            throw new RuntimeException(e);
        }
        return this.num--;
    }

    public int getTest(){
        return this.num;
    }
}

Testクラスの動作は下記のように確認を行った。 ExecutorService クラスを用いて、2つのスレッドを生成し、synchronized を用いて排他制御を実装している。

  • スレッド1ではtestインスタンスに対してsynchronized を用いて排他制御し処理を実行。
  • スレッド2ではスレッド1と同様の処理を実行。

スレッド1とスレッドで同じインスタンスにアクセスしないように排他制御を用いている。 そのためスレッド1で処理が完了後にスレッド2の処理が実行されることが期待される。

public class Main {
    public static void main(String[] args) {
        Test test = new Test();

        ExecutorService exec = Executors.newFixedThreadPool(2);
        // スレッド1
        exec.submit(()-> {
            synchronized (test){
                for(int i=0;i<5;i++){
                    System.out.println("sub1["+test.addTest()+"]");
                }
            }
        });
        // スレッド2
        exec.submit(()-> {
            synchronized (test){
                for(int i=0;i<5;i++){
                    System.out.println("sub2["+test.addTest()+"]");
                }
            }
        });
    }
}

実行結果

sub1[100]

sub1[101]

sub1[102]

sub1[103]

sub1[104]

sub2[105]

sub2[106]

sub2[107]

sub2[108]

sub2[109]

同じインスタンスに対して排他制御を用いた結果、並列処理ではあるものの順次実行のようになっている。

デッドロック

クラスTestを作成した。 こちら競合の確認で用いたものと同じであるため説明は割愛する。 ……

class Test {
    private int num = 100;

    public int addTest(){
        try {
            Thread.sleep(1000);
        }catch (InterruptedException e){
            throw new RuntimeException(e);
        }
        return this.num++;
    }

    public int subTest(){
        try {
            Thread.sleep(1000);
        }catch (InterruptedException e){
            throw new RuntimeException(e);
        }
        return this.num--;
    }

    public int getTest(){
        return this.num;
    }
}

Testクラスの動作は前回の記事デッドロックを解消する形で確認を行った。

  • スレッド1ではtest1⇒test2の順番にロックと処理を実施。
  • スレッド2ではtest2⇒test1の順番にロックと処理を実施。 ↓修正後
  • スレッド1ではtest1⇒test2の順番にロックと処理を実施。
  • スレッド2ではtest1⇒test2の順番にロックと処理を実施。
public class Main {
    public static void main(String[] args) {
        Test test1 = new Test();
        Test test2 = new Test();

        ExecutorService exec = Executors.newFixedThreadPool(2);
        // スレッド1
        exec.submit(()-> {
            synchronized (test1){
                System.out.println("sbmit1[test1="+test1.getTest() + "]");
                test1.addTest();
                System.out.println("sbmit1[test1="+test1.getTest() + "]");
                synchronized (test2){
                    System.out.println("sbmit1[test2="+test2.getTest() + "]");
                    test2.subTest();
                    System.out.println("sbmit1[test2="+test2.getTest() + "]");
                }
            }
        });
        // スレッド2
        exec.submit(()-> {
            synchronized (test2){
                System.out.println("sbmit2[test2="+test2.getTest() + "]");
                test2.addTest();
                System.out.println("sbmit2[test2="+test2.getTest() + "]");
                synchronized (test1){
                    System.out.println("sbmit2[test1="+test1.getTest() + "]");
                    test1.subTest();
                    System.out.println("sbmit2[test1="+test1.getTest() + "]");
                }
            }
        });
    }
}

実行結果

sbmit1[test1=100]

sbmit1[test1=101]

sbmit1[test2=100]

sbmit1[test2=99]

sbmit2[test1=101]

sbmit2[test1=102]

sbmit2[test2=99]

sbmit2[test2=98]

実行結果を見て貰えば分かる通り、実行できていなかった処理も実行できているためデッドロックが解消できていることが分かる。 排他制御を実装する場合は、処理順序に気を遣う必要があることが分かった。