未来エンジニア養成所Blog

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

【Java】マルチスレッド Part3

title

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

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

phoeducation.work


スレッドの制御

スレッドの動きはJavaVMが決定しますが、プログラマはThreadクラスのいくつかのメソッドを使用してスレッドの動きに影響を与えることができます。

ただし完全に制御できるものではありません。

下の表はThreadクラスに定義されている、スレッドの動きを制御する(影響を与える程度)主なメソッドです。


【スレッドを制御するThreadクラスのメソッド】

メソッド名 説明
public static void sleep(long millis) throws InterruptedException このメソッドを呼び出したスレッドがmillisミリ秒休止する
(スリープ状態→実行可能状態)
public final void join() throws InterruptedException

public final void join(long millis) throws InterruptedException
指定したスレッドが終了するまで、このメソッドを読み出したスレッドを待機させる
(待機状態→実行可能状態)
public static void yield() このメソッドを呼び出したスレッドを一時的に休止し、他のスレッドに実行の機会を与える
(実行中状態→実行可能状態)


sleep()メソッドは、このメソッドを呼び出したスレッドを指定した時間一時的に休止させるメソッドです。

スレッドの状態は「実行中状態」から「スリープ状態」になり、指定した時間経過すると「実行可能状態」になります。

staticメソッドなので、Thread.sleep()と指定し、()内の引数にはミリ秒単位で休止する時間を指定します。

1ミリ秒とは1/1000秒の事で、言いかえると1秒は1000ミリ秒です。

指定した時間経過したからといって即「実行中状態」になるのではなく、「実行可能状態」になったものがJavaVMからの命令を受けて「実行中状態」になります。


join()メソッドは、指定したスレッドが終了するまでこのメソッドを呼び出したスレッドを待機させるものです。

引数に、時間を指定することもできます。

join()メソッドによって、「待機状態」に入ったスレッドは、指定したスレッドが終了したか、指定した時間経過すると「実行可能状態」になります。

そのあとJavaVMからの命令をうけて「実行中状態」になります。

これらのsleep()メソッドやjoin()メソッドはInterruptedExceptionという例外を発生する可能性があるので、メソッドを使用する際にはtry~catch文などで例外処理を記述する必要があります。


yield()メソッドは、このメソッドを呼び出したスレッドを一時的に休止し、他のスレッドに実行の機会を与えます。

sleep()やjoin()とは異なり、yield()メソッドで休止されたスレッドは、「待機状態」や「スリープ状態」になるのではなく、すぐに「実行可能状態」になります。他に実行可能状態のスレッドがない場合や、他のスレッドの方が優先順位が低い場合は、JavaVMが「実行中状態」にします。

下図にある、wait()、notify()は別途説明します。


状態遷移


【sleep()メソッドの使用】

public class ThreadSample04 {
    public static void main(String[] args) {

        MyThread4 mt = new MyThread4();
        mt.start();

        try{
             Thread.sleep(10000);
        }catch(InterruptedException e){}

        for(int i = 1; i <= 10 ; i++){
            System.out.println(i + "回目のmain()の処理です");
        }
    }
}
class MyThread4 extends Thread{
    public void run(){
        for(int i = 1; i <= 10 ; i++){
            System.out.println(i + "回目のMyThread4の処理です");
        }
    }
}


実行結果

1回目のMyThread4の処理です
2回目のMyThread4の処理です
3回目のMyThread4の処理です
4回目のMyThread4の処理です
5回目のMyThread4の処理です
6回目のMyThread4の処理です
7回目のMyThread4の処理です
8回目のMyThread4の処理です
9回目のMyThread4の処理です
10回目のMyThread4の処理です
1回目のmain()の処理です
2回目のmain()の処理です
3回目のmain()の処理です
4回目のmain()の処理です
5回目のmain()の処理です
6回目のmain()の処理です
7回目のmain()の処理です
8回目のmain()の処理です
9回目のmain()の処理です
10回目のmain()の処理です


7行目~9行目のtry~catch文の中で、Threadクラスのsleep()メソッドを使用し、10秒間スレッドを休止させています。

sleep()メソッドを呼び出しているメソッドはmainスレッドなので、mainスレッドが10秒間処理を休止している間に、MyThread4のスレッドが動いているのがわかります。


【join()メソッドの使用】

public class ThreadSample05 {
    public static void main(String[] args) {

        MyThread5 mt = new MyThread5();
        mt.start();

        try{
            mt.join();
        }catch(InterruptedException e){}

        for(int i = 1; i <= 10 ; i++){
            try{
                Thread.sleep(1000);
            }catch(InterruptedException e){}
            System.out.println(i + "回目のmain()の処理です");
        }
    }
}
class MyThread5 extends Thread{
    public void run(){
        for(int i = 1; i <= 10 ; i++){
            System.out.println(i + "回目のMyThread5の処理です");
        }
    }
}


実行結果

1回目のMyThread5の処理です
2回目のMyThread5の処理です
3回目のMyThread5の処理です
4回目のMyThread5の処理です
5回目のMyThread5の処理です
6回目のMyThread5の処理です
7回目のMyThread5の処理です
8回目のMyThread5の処理です
9回目のMyThread5の処理です
10回目のMyThread5の処理です
1回目のmain()の処理です
2回目のmain()の処理です
3回目のmain()の処理です
4回目のmain()の処理です
5回目のmain()の処理です
6回目のmain()の処理です
7回目のmain()の処理です
8回目のmain()の処理です
9回目のmain()の処理です
10回目のmain()の処理です


7行目~9行目のtry~catch文の中で、Threadクラスのjoin()メソッドを使用しています。

join()メソッドは指定したスレッドが終了するまで、このメソッドを呼び出したスレッドを待機させるので、この例ではMyThead5のスレッドの処理が終了するまで、これを呼び出したmainスレッドを待機させます。

実行結果を見ると、MyThread5の処理が10回行われるまで、mainスレッドの処理が待機し、MyThread5の処理が終わった後に、mainスレッドの処理が1秒ごとsleep()メソッドで休止しながら実行されているのがわかります。


【yield()メソッドの使用】

public class ThreadSample06 {
    public static void main(String[] args) {

        MyThread6 mt = new MyThread6();
        mt.start();

        Thread.yield();

        for(int i = 1; i <= 10 ; i++){
            try{
                Thread.sleep(1000);
            }catch(InterruptedException e){}
            System.out.println(i + "回目のmain()の処理です");
        }
    }
}
class MyThread6 extends Thread{
    public void run(){
        for(int i = 1; i <= 10 ; i++){
            System.out.println(i + "回目のMyThread6の処理です");
        }
    }
}


実行結果

1回目のMyThread6の処理です
2回目のMyThread6の処理です
3回目のMyThread6の処理です
4回目のMyThread6の処理です
5回目のMyThread6の処理です
6回目のMyThread6の処理です
7回目のMyThread6の処理です
8回目のMyThread6の処理です
9回目のMyThread6の処理です
10回目のMyThread6の処理です
1回目のmain()の処理です
2回目のmain()の処理です
3回目のmain()の処理です
4回目のmain()の処理です
5回目のmain()の処理です
6回目のmain()の処理です
7回目のmain()の処理です
8回目のmain()の処理です
9回目のmain()の処理です
10回目のmain()の処理です


7行目で、Threadクラスのyield()メソッドを使用しています。

yield()メソッドは、このメソッドを呼び出したスレッドを一時的に休止し、他のスレッドに実行の機会を与えます。

この例では、mainスレッドがyield()メソッドを呼び出しているため、mainスレッドがMyThread6スレッドに実行の機会を譲ります。

実行結果を見ると、yield()メソッドで実行の機会を与えられたMyThread6が先に実行された後、mainスレッドの処理が1秒ごとsleep()メソッドで休止しながら、実行されていることがわかります。


スレッドの状態と制御
sleep()、join()、yield()により実行不可能になったスレッドは、「実行可能状態」になった後、即「実行中状態」になるとは限りません。

JavaVMによるスレッドスケジューラがスレッドの状態を管理しているため、プログラマはスレッドの状態を完全に制御できるわけではありません。


スレッドの競合

マルチスレッドは同時に複数の処理ができて便利です。

しかし、複数のスレッドを他のスレッドの事を考慮せずに動かしてしまうと、場合によっては大きな問題を引き起こします。


例えばこのような例を考えてみましょう。

2つのスレッドhusbandとwifeが存在します。

moneyというデータを共有した場合、つまり同じオブジェクトを利用しています。

moneyの中の残高というフィールドが3000のときに、husbandとwifeがそれぞれ持っていた現金という変数が-2000だったとします。


スレッドの競合


2つのスレッドがmoneyから2000ずつ引くと、残高はマイナスになってしまいます。

このようなときに「マイナスになると良くないので、残高をチェックして十分にあるときだけデータを取得しよう」という処理をしたとします。

すると、どちらか先に実行したスレッドが2000を取得でき、後に実行されたスレッドは残高が1000しかないので取得できません。


スレッドの競合


【マルチスレッドの競合】

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

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

    public 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 + "円です");
            return -request;
        }else{
            System.out.println("お金が足りません");
            System.out.println("残高は" + balance + "円です");
            return 0;
        }
    }
}

class Person extends Thread{
    //銀行口座
    private BankAccount account;
    //今持っている現金
    private int cash;

    public Person(BankAccount m , int c){
        account = m;
        cash = c;
    }
    //銀行にお金を取りに行く
    public void run(){
        System.out.println("現金が"+ cash + "なので銀行に行きます");
        //とってきたお金を所持金に加える
        cash += account.getMoney(cash);
        System.out.println("現金が"+ cash + "円になりました");
    }
}

public class InTheBank {
    public static void main(String[] args) {
        BankAccount account = new BankAccount(3000);
        Person husband = new Person(account, -2000);
        Person wife = new Person(account, -2000);

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


実行結果

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


スレッドの競合

まず全体の流れを見てみましょう。

1行目~25行目のBankAccountクラスは銀行と考えます。

銀行にはhusbandとwifieが共有して使っているbalance(通帳)があります。

4行目~6行目のコンストラクタでBankAccountクラスのオブジェクトが作成されたときに、通帳残高が設定されます。

今回の残高は3000円です(48行目)。

8行目~24行目のgetMoney()メソッドは銀行取引の処理です。

getMoney()メソッドを呼び出す際に、引数にhusbandとwifeがそれぞれ持っている現金(取引したい金額)を取引金額(request)に設定します。

銀行残高(balance)が取引金額(request)以上にあれば、残高(balance)から要求金額(request)を引いて、処理された要求金額にマイナスをつけてgetMoney()メソッドの戻り値として返します。

銀行残高(balance)が取引金額(request)に満たなければ、「お金が足りません」と表示し、また現在の残高(balance)を表示し、戻り値を0円とします。


27行目~44行目のPersonクラスはThreadクラスを継承したクラスなので、Personクラスのインスタンスはマルチスレッドとなります。33行目~36行目のコンストラクタで、Personクラスのオブジェクト(husbandとwife)が生成されたとき、銀行口座(account)のオブジェクトを格納しておくための変数と、現在持っている現金(cash)を引数として設定しておきます。

38行目~43行目のrun()メソッドにマルチスレッドが呼び出された時、すなわち、start()メソッドが実行されたときの処理を記述しています。

41行目で銀行取引をするgetMoney()メソッドを呼び出し、引数に自分の持っている現金(cash)を指定して実行して、処理された金額としての戻り値を、現在の現金(cash)に加算、減算することで、husbandとwifeの取引結果を表示します。


46行目~55行目のInTheBankクラスにはmain()メソッドがあるので、ここから処理がスタートします。

48行目でBankAccount(銀行口座)のインスタンスを生成し、残高(balance)を3000円に設定します。

49行目~50行目はPersonクラスのマルチスレッドhusband、wifeを生成します。

その際にコンストラクタで、48行目で作成した銀行口座(account)をそれぞれ引数に指定することでhusbandとwifeから共通に使用できるオブジェクトにしています。

またそれぞれ現在持っている現金(cash)も引数として渡してgetMoney()メソッドでの取引に使用します。

52行目~53行でhusbandとwifeスレッドからstart()メソッドを呼び出し、38行目~43行のrun()メソッドが実行されます。


処理の流れが把握できたら、次にマルチスレッドの動きを見ていきましょう。


BankAccount.javaの10行目には、if文による値のチェックがあります。

残高と要求の和が負にならないことを確認しているはずです。

それなのに、なぜか残高がマイナスになってしまいました。

こういったことがマルチスレッドでは起こりがちです。

一体何が起こったのでしょうか?


2つのスレッドが同時に処理を行っています。

if文のチェックをここでは残高照会と呼ぶことにします。

先に実行されたスレッドが残高照会を行ってから取引するまでの間に、もう片方のスレッドが残高照会を行うと、まだ取引は誰も行っていないので残高は3000です。

そのため、両方のスレッドがif文で真と判定された処理を実行します。


スレッドの競合


その後は自動的に残高の値を変更してしまうので、値が負になってしまったのです。

このプログラムでは「残高照会で十分な金額があるときのみ取り引きできる」という仕様になっていなければいけないはずです。

ここで、残高照会(BankAccountクラスの10行目のif文)と取引(BankAccountクラスの15行目)は「アトミック(atomic)」な処理です。

アトミックとは「分割できない」つまり、連続して行わなければいけないということを表します。

残高照会をして十分なお金があっても、他に口座を使える人がいれば1年後にはどうなっているか分かりません。

実際には次の瞬間にでも、同じ額のお金があるという保証はありません。


マルチスレッドを利用する際には、1つのオブジェクトに複数のスレッドが同時にアクセスをするとこのような現象が起こります。これをスレッドの「競合状態」と言います。


「マルチスレッド Part4」へ続きます。 phoeducation.work


参考図書



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



あわせて学習したい

phoeducation.work phoeducation.work