未来エンジニア養成所Blog

プログラミングを皆に楽しんでもらうための情報をお届けします。

【Java】マルチスレッド Part5

title

前回に引き続きマルチスレッドの解説です。

前回の記事はこちら。 phoeducation.work phoeducation.work phoeducation.work phoeducation.work


マルチスレッドと同期制御

スレッド同士でやり取りをしても良いのですが、多くのスレッドがあったときには管理が煩雑になります。

そのため、「モニタ」で管理します。

BankAccountオブジェクトで残高が足りないときは、そのオブジェクトを参照しているスレッドを止めることができます。

また、そのスレッドを再開することもできます。

これには、Objectクラスのメソッドwait( )メソッドnotify( )メソッドを用います。Objectクラスの機能なので、すべてのクラスにはこの機能があります。


モニタ
モニタとはスレッドの実行権限の事を指します。

例えば「スレッドがモニタを取得している」という表現は、スレッドが現在実行権限をもっているという事を意味します。


同期制御
wait()やnotify()でスレッドのタイミングをはかることを、同期制御と言います。


Objectクラスのwait()メソッドとnotify()メソッド、notifyAll()メソッド
wait()メソッドは他のスレッドがこのオブジェクトをnotify()もしくはnotifyAll()を呼び出すまで、現在のスレッドを待機させます。

wait()メソッドはInterruptedException例外を発生する可能性があるので、try~catchブロックで必ず例外処理を行います。

synchronized指定されたスレッドはロックされている状態なので、wait()を実行すると、自分のロックを解放して実行待機状態に入り、他のスレッドにロックするチャンスを与えます。

言いかえるとwait()はロックを解放するので、synchronizedブロックの中で指定しなければなりません。


notify()、notifyAll()メソッドは、wait()で待機されたスレッドを再開します。

これらは例外処理の必要はありません。

スレッドのメソッド


【同期制御】

class BankAccount2{        //銀行
    private int balance; //通帳残高

    public BankAccount2(int b){   //BankAccountオブジェクト生成時に実行されるコンストラクタ
        balance = b;
    }

    public synchronized int getMoney(int request){
        //残高が要求以上にあれば
        if(balance + request >= 0){
            try{
                Thread.sleep(100);
            }catch(InterruptedException e){}
            //取引を行う
            balance += request;
            System.out.println(request + "円取引されました");
            System.out.println("残高は" + balance + "円です");
            //wait()メソッドで止められたスレッドを再開させる
            notify();
            return -request;
        }else{
            System.out.println("お金が足りません");
            System.out.println("残高は" + balance + "円です");
            //このオブジェクトを参照しているスレッドを一時停止する
            try{
                wait();
            }catch(InterruptedException e){}
            return 0;
        }
    }
}
class Person2 extends Thread{
    //銀行口座
    private BankAccount2 account;
    //今持っている現金
    private int cash;

    public Person2(BankAccount2 m , int c){
        account = m;
        cash = c;
    }
    //銀行にお金を取りに行く
    public void run(){
        System.out.println("現金が"+ cash + "なので銀行に行きます");
        do{

            //とってきたお金を所持金に加える
            cash += account.getMoney(cash);
        }while(cash < 0);
        System.out.println("現金が"+ cash + "円になりました");
    }

}
public class InTheBank2 {
    public static void main(String[] args) {
        BankAccount2 account = new BankAccount2(0);
        Person2 husband = new Person2(account, -2000);
        Person2 wife = new Person2(account, 3000);

        husband.start();
        wife.start();
    }
}


実行結果

現金が-2000なので銀行に行きます
現金が3000なので銀行に行きます
お金が足りません
残高は0円です
3000円取引されました
残高は3000円です
現金が0円になりました
-2000円取引されました
残高は1000円です
現金が0円になりました


今度は最初の余分な処理がありません。

まずhusbandはお金が足りないのでwait( )メソッドを実行し、一時停止状態になります。

このときsynchronizedメソッド内で停止しているのですが、wait( )メソッドの性質でロックを解放します。

するとwifeがgetMoney( )メソッドを呼び出し、こちらは取引できます。

取引をするとreturn文の前にnotify( )メソッドがあるのでこれを呼び出しますが、notify( )を呼び出すと停止状態のスレッドが再開します。

つまりhusbandが再開します。

同期制御


wait( )メソッドを使用するとロックを解放するので、競合を起こす可能性があります。

そのため、wait( )メソッドはsynchronizedメソッド内で使わなければなりません。


staticメンバの排他制御

これまで見てきた排他制御は共通オブジェクト内のインスタンス変数の整合性を保つものでした。(通帳の残高を表すBankAccountクラスのaccountオブジェクト)

ここでは、static変数が複数のスレッドからアクセスされたときにどのように整合性を保つのかを見てみましょう。

staticメソッドでsynchronizedキーワードを使用すると、static変数の値の排他制御を行うことができます。

インスタンス変数の排他制御はインスタンス変数が存在するオブジェクトをsynchronizedでロックすることで排他制御を行うことができました。

static変数ではstatic変数が存在するクラスオブジェクトをロックすることで排他制御を行います。

staticメソッドをsynchronizedするには、次のように記述します。

  • synchronized static 戻り値 メソッド名(引数リスト){ }
  • synchronized(クラスオブジェクト){ }


クラスオブジェクトを取得するには、次のように記述します。

  • クラス名.class


【static変数の排他制御】

class MyRunnable implements Runnable{
    static int a;
    static int b;
    public static String printAB(){
        a++;
        try{
            Thread.sleep(1000);
        }catch(InterruptedException e){}
        b++;
        return "a=" + a + " b=" + b;
    }

    public void run(){
        for(int i = 1; i <=10; i++){
            System.out.println(Thread.currentThread().getName() + ":" + printAB());
        }
    }
}

public class ThreadSample07 {
    public static void main(String[] args) {
        MyRunnable mythread = new MyRunnable();
        Thread thread_a = new Thread(mythread);
        Thread thread_b = new Thread(mythread);
        thread_a.start();
        thread_b.start();
    }
}


実行結果

Thread-1:a=2 b=1
Thread-0:a=2 b=1
Thread-1:a=4 b=2
Thread-0:a=4 b=2
Thread-1:a=6 b=3
Thread-0:a=6 b=3
Thread-0:a=8 b=4
Thread-1:a=8 b=4
Thread-1:a=10 b=5
Thread-0:a=10 b=5
Thread-0:a=12 b=6
Thread-1:a=12 b=6
Thread-0:a=14 b=8
Thread-1:a=14 b=8
Thread-1:a=16 b=9
Thread-0:a=16 b=9
Thread-1:a=18 b=11
Thread-0:a=18 b=11
Thread-1:a=20 b=12
Thread-0:a=20 b=12


static変数aとbは、クラスに1つだけ存在します。

thread_aとthread_bという2つのスレッドからアクセスされ、それぞれインクリメントされています。

このプログラムでは、synchronizedで排他制御をしていないため、static変数aとbは、両スレッドからアクセスされ、aとbがばらばらにインクリメントされてしまいます。

このような状態を「競合している」と言います。

排他制御


以下のようにsynchronizedブロックを挿入してみます。

class MyRunnable implements Runnable{
    static int a;
    static int b;
    public static String printAB(){
        synchronized(MyRunnable.class){
            a++;
            try{
                Thread.sleep(1000);
            }catch(InterruptedException e){}
            b++;
            return "a="+ a + " b=" + b;
        }
    }

    public void run(){
        for(int i = 1; i <=10; i++){
            System.out.println(Thread.currentThread().getName() + ":" + printAB());
        }
    }
}

public class ThreadSample07 {
    public static void main(String[] args) {
        MyRunnable mythread = new MyRunnable();
        Thread thread_a = new Thread(mythread);
        Thread thread_b = new Thread(mythread);
        thread_a.start();
        thread_b.start();
    }
}


実行結果

Thread-0:a=1 b=1
Thread-1:a=2 b=2
Thread-0:a=3 b=3
Thread-1:a=4 b=4
Thread-0:a=5 b=5
Thread-1:a=6 b=6
Thread-0:a=7 b=7
Thread-1:a=8 b=8
Thread-0:a=9 b=9
Thread-1:a=10 b=10
Thread-0:a=11 b=11
Thread-1:a=12 b=12
Thread-0:a=13 b=13
Thread-1:a=14 b=14
Thread-0:a=15 b=15
Thread-1:a=16 b=16
Thread-0:a=17 b=17
Thread-1:a=18 b=18
Thread-0:a=19 b=19
Thread-1:a=20 b=20


static変数aとbは、クラスに1つだけ存在しますが、1つのスレッドがアクセスしている間は、synchronized指定によりロックされているので、他のスレッドからアクセスされないので、1つのスレッド実行中に変数aとbが同時にインクリメントされており、排他制御されていることがわかります。


まとめ

  • マルチスレッドの利用

    • スレッドとはアプリケーションが実行される際の処理の流れを表す最小単位です。
    • どのようなアプリケーションでも最低1つのスレッドが存在します(mainスレッド)。
    • マルチスレッドとはアプリケーションを複数のスレッドで分割して実行させることです。
    • プログラマはスレッドの順番を制御できません。
  • スレッドの制御と開始

    • Threadクラスを継承したサブクラスを作って、run()メソッドを実装し、start()でrun()を呼び出します。
    • Runnableインターフェースを実装したサブクラスを作成し、run()を実装する。このクラスのインスタンスをThreadクラスのコンストラクタの引数に指定して、start()でrun()を呼び出します。
  • スレッドの状態

    • 初期状態、実行可能状態、実行中状態、待機状態、ブロック状態、スリープ状態、終了状態があります。
    • 次にどのスレッドを実行するかはJavaVMの機能の一部であるスレッドスケジューラが管理します。
    • スレッドの状態に影響を与えることはできるが、スレッドの状態を制御することはできません。
  • スレッドの優先順位

    • 各スレッドは優先順位(1~10)が設定されており、デフォルトは5です。
    • 実行可能状態のスレッドが競合した場合、一般的に高い優先順位を持ちスレッドが優先的に実行されます。
    • スレッドの実行は、JavaVMに依存しているため、優先度の高いスレッドから先に実行される保証はありません。
  • スレッドの制御

    • Threadクラスにはスレッドの処理を一時休止したり、他のスレッドを待ち合わせするなどスレッドを制御するメソッドが用意されています。
    • sleep()、join()、yield()
  • スレッドの競合

    • 複数のスレッドが同じオブジェクトを操作している場合、データの整合性が取れなくなることを「競合している」と言います。
  • マルチスレッドの排他制御

    • あるスレッドが処理を行っている間、他のスレッドが入らないように独占して実行します。
    • 排他制御を行うには、操作されるオブジェクトにsynchronizedを付けるか、もしくは操作する側のメソッドにsynchronizedを付けます。
  • マルチスレッドとデッドロック

    • 2つ以上のスレッドが互いにロックを取得し、互いのロックの解除待ちになっている状態です。
    • どちらかのロックが解除されない限り、どちらのスレッドも実行されず、2つのスレッドは永久に待機状態となります。
    • デッドロックのタイミングをコンパイル時点で検出することはできません。
    • 実行時に例外で通知しないので、プログラムで制御しなくてはなりません。
  • マルチスレッドと同期制御

    • スレッド同士の実行のタイミングを合わせることです。
    • Objectクラスのwait()、notify()、notifyAll()を使用すると、メソッドの処理の順番を操作することができます。
    • wait()はロックが取得された状態(synchronizedメソッドもしくはブロック中)で使用します。
    • ロックを取得していない状態で使用すると、IllegalMonitorStateExceptionが発生します。
  • staticメンバの排他制御

    • staticメソッドでsynchronizedを使用するとクラスオブジェクト自体がロックされます(static変数がロックされる)。
    • クラスオブジェクトをロックするには、staticメソッドにsynchronizedを付けるか、syncrhonized(クラスオブジェクト)と指定をします。
    • クラスオブジェクトを取得するには「クラス名.class」と記述します。


参考図書



独学で挫折しそうになったら、オンラインプログラミングスクール
未来エンジニア養成所Logo



あわせて学習したい

phoeducation.work phoeducation.work