ほぷしぃ

Java言語入門 〜C言語を学んだ君へ〜

[第14回]マルチスレッド

[1] 排他制御

マルチスレッドはこのままでは、問題があります。
それは、ある処理が終わるまで他の処理をしてほしくない場合に、勝手に処理をしてしまうことです。
同時に実行できるようになったことで素早く処理ができるようになりましたが、そのために問題がでてしまいました。
イメージで説明すると、電車にみんなが一斉に駆け込んでいる状態と同じです。
けがをする危険性もあり、良くありません。
順番に並び電車に入れば危険性が低くなり、電車に乗ることができます。

そのような勝手な処理を防ぐために、排他制御を行います。
排他制御を行うことで順番を守ることができるのです。
プログラムで言うと、複数のスレッドの処理順番を決めることになります。

排他制御を行う方法として、synchronizedがあります。
これはメソッドで使う場合と、文で使う場合の2種類があります。

メソッドと文の比較

種類 範囲
メソッド メソッド内全て
自由に指定


メソッドにつける場合にはメソッド全体に影響を与えます。
文につける場合にはメソッドにつけた場合に比べて細かく範囲を指定できます。
また、2つともstaticがあるかないかで少し異なります。

staticの違い

staticがある場合とない場合の違いは何に対してロックがかけられるかです。
ロックとは、既に利用されていて実行できない状態のことです。
このロックが解除された時に実行できるようになります。
では、2つは何を基準に判断するのでしょうか。以下の表を見てください。

static 排他処理の基準
なし 同じインスタンス
あり 同じクラス

staticがない場合には、ロックがインスタンスに対して設定されています。
このインスタンスを所持しているほうに、実行する権利が与えられます。
ないスレッドは既に所持しているスレッドの実行が終わるまで待ちます。

staticがある場合には、クラスに対して設定されています。
そのクラスが利用されていれば、待つ必要があります。

[2] メソッド(synchronized)

では、メソッドの「synchronized」から見ていきましょう。

メソッドの形式

メソッドで使う場合には次のように書きます。

修飾子 synchronized 戻り値 メソッド名 (引数){…}

次はプログラムの書き方です。
なお、プログラムではstaticを使用しません。

メソッドのsynchronizedプログラム


1、あるクラスのインスタンスを生成
2、作成したインスタンスをスレッドのコンストラクタの引数として渡す
    (スレッドの作成方法は前ページを参照してください)
3、それぞれのスレッドを実行する

staticをつけないと、判断材料はインスタンスになります。
まず判断材料のクラスをインスタンス化し、そのオブジェクトを各スレッドに渡します。

test obj = new test(); test2 tt1 = new test2(obj,);
では、実際にプログラムで動作を確認してみましょう。

サンプルプログラム

class test{
    private static int var1 = 20;
    
    public synchronized void decrease(String name) {
        
        try {
                var1--;
                System.out.print("var1 ="+ var1);
                Thread.sleep(1000);
                
        } catch(InterruptedException e) {
                System.out.println(e);
        }finally{
                System.out.println(" "+name+"の処理が終了");
        }
    }
}

class test2 extends Thread{
    test obj;
    String name;
    
    test2(test obj, String name){
        this.obj = obj;
        this.name = name;
    }
    
    public void run() {
        for(int i = 0; i < 5; i++){
            obj.decrease(name);
        }
    }
}

public class Java14_03 {
    public static void main(String args[]) {
        test obj = new test();
        test2 tt1 = new test2(obj, "A");
        test2 tt2 = new test2(obj, "B");
        test2 tt3 = new test2(obj, "C");
        test2 tt4 = new test2(obj, "D");
        
        System.out.println("開始");
        
        tt1.start();
        tt2.start();
        tt3.start();
        tt4.start();
        
        try{
            tt1.join();
            tt2.join();
            tt3.join();
            tt4.join();
        }catch(InterruptedException e){}
        System.out.println("終了");
    }
}

実行結果

メソッドのsynchronized実行結果

一定の順番で処理が実行されるようになりました。
main内の、test obj = new test();で、インスタンス化をしています。
インスタンス化した、objを各スレッドの引数として渡しています。
sychronizedを使うことにより順番に処理を実行してくれます。
sychronizedがない場合にはどうなるのか、実際に試してみてください。
それぞれのスレッドが好き勝手に処理をしています。

start()メソッドの後に例外処理がありますが、
この中に書かれているjoin()というメソッドがあります。
これは、各スレッドが終了するまでこの先の処理を行わないというメソッドです。
そのために、最後のSystem.out.println("終了")の文は、全てのスレッドが終了してから実行されます。
tryからcatch文までを、全てコメントアウトなり削除なりして、試してみてください。
先にSystem.out.println("終了")が実行されるはずです。

[3] 文(synchronized)

次に文の場合のsynchronizedを見ていきましょう。

文の形式

synchronized(インスタンス){ 排他処理}

ブロック内「{…}」の処理が複数のスレッドで同時に実行されなくなります。
後は誰が処理する権利を持っているかで実行順序が決まります。
メソッドの場合と違い、プログラマーが自分で任意の範囲を指定できます。

次はプログラムの書き方です。
先ほどのプログラムを少しだけ改良しています。

文のsynchronizedプログラム

class test{
    private static int var1 = 20;
    
    public void decrease(String name, test obj) {
        
        synchronized(obj){
            try {
                
                    var1--;
                    System.out.print("var1 ="+ var1);
                    Thread.sleep(1000);
                
                
            }catch(InterruptedException e) {
                System.out.println(e);
            }finally{
                System.out.println(" "+name+"の処理が終了");
            }
        }
    }
}

class test2 extends Thread{
    test obj = null;
    String name;
    
    test2(test obj, String name){
        this.obj = obj;
        this.name = name;
    }
    
    public void run() {
        for(int i = 0; i < 5; i++){
            obj.decrease(name, obj);
        }
    }
}

public class Java14_04 {
    public static void main(String args[]) {
        test obj = new test();
        test2 tt1 = new test2(obj, "A");
        test2 tt2 = new test2(obj, "B");
        test2 tt3 = new test2(obj, "C");
        test2 tt4 = new test2(obj, "D");
        
        System.out.println("開始");
        
        tt1.start();
        tt2.start();
        tt3.start();
        tt4.start();
        
        try{
            tt1.join();
            tt2.join();
            tt3.join();
            tt4.join();
        }catch(InterruptedException e){}
        System.out.println("終了");
    }
}

実行結果

文のsynchronized実行結果

先ほどのソースを少しだけ変更しただけです。
文の場合には、排他処理を行いたい場所を囲うことでできます。
文のほうが、細かく設定できるという利点があります。

[4] マルチスレッドの注意点

ここでは、基本的なことだけを学習しました。
マルチスレッドの注意点は実際に実行される順序が一定でないことです。
プログラムが実行されるたびに変更されます。
そのために、「synchronized」を使って順番を決めるのです。
お互いが処理の順番を守ることで予定していた処理を行うことができます。
しかし、排他処理の場合には間違えてデッドロックになってしまう危険性もあります。

デッドロック

デッドロック

上の図を参考に説明します。
処理は上から下に流れているとします。

そして、2つのスレッドがあります。スレッドAとスレッドBです。
スレッドAがCを所持、スレッドBがDを所持(ロック)していたとします。
このC,Dを所持しているとある処理が行える「優先権がある」と考えてください。
この時にスレッドAはDが解放されるまで待機、それまでCを持っている。
同じようにスレッドBもCが解放されるまで待機、それまでDを持っている。
このような状態が発生してしまうとこれ以上処理が行われなくなってしまいます。
この状態をデッドロックと呼んでいます。
プログラムが複雑になるとマルチスレッドはとても難しいです。



前のページへ ページのトップへ 第1問-問題へ