社会人のメモ帳

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

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

PREV | LIST | NEXT

並列処理でエラーを起こしてみる

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

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

スレッドの無駄遣い

エラーの枠に入れているが、一見すると挙動には影響がないので今回は割愛。

競合

クラスTestを作成した。 変数numについて、初期値100にインクリメントするaddTestメソッドを作成している。

class Test {
    private int num = 100;

    public int addTest(){
        return this.num++;
    }
}

addTestの引数に0から4の値を順番に入れていく処理を、Threadで実装して2つを同時に実行した。 Threadが1つの場合は、100、101、102、103、104と表示されていくことが期待されているが、 2つのThreadでそれぞれ変数numにアクセスしているため値の遷移がおかしくなっている。

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

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run(){
                for(int i=0;i<5;i++){
                    System.out.println("sub1["+test.addTest()+"]");
                }
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run(){
                for(int i=0;i<5;i++){
                    System.out.println("sub2["+test.addTest()+"]");
                }
            }
        });
        t1.start();
        t2.start();
        System.out.println("main");
    }
}

実行結果

main

sub1[100]

sub1[102]

sub1[103]

sub1[104]

sub1[105]

sub2[101]

sub2[106]

sub2[107]

sub2[108]

sub2[109]

実行結果を見て貰うと分かる通り、sub1(=t1の値)は100, 102, 103, 104, 105というように101を飛ばしてカウントされてしまっている。

デッドロック

クラス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 を用いて排他制御を実装している。

test1とtest2、2つのインスタンスに対して、それぞれsynchronized を用いてロックするタイミングが違う。

  • スレッド1ではtest1⇒test2の順番にロックと処理を実施。
  • スレッド2ではtest2⇒test1の順番にロックと処理を実施。

スレッド1とスレッド2が並列で処理が行われることを考えると、スレッド1でtest2のインスタンスにアクセスしようとするタイミングではスレッド2でロックを実施した後となる。 逆もまた同様、スレッド2でtest1のインスタンスにアクセスしようとするタイミングではスレッド1でロックを実施した後となる。

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]

sbmit2[test2=100]

sbmit2[test2=101]

sbmit1[test1=101]

実行結果を見て貰えば分かる通り、スレッド1とスレッド2はそれぞれ2つ目のインスタンスアクセスのためのロック処理で失敗していることが分かる。 (sbmit2[test1~ 、sbmit1[test2~ 、がそれぞれ表示されていないため)