前回に引き続きマルチスレッドの解説です。
前回の記事はこちら。 phoeducation.work phoeducation.work phoeducation.work
マルチスレッドの排他制御
スレッドの競合とは、複数のスレッドが同時に同じ場所を実行するのが原因でした。
では、スレッドの競合を避けるにはどうしたら良いでしょうか。
あるスレッドがアトミックな処理を行っているとき、他のスレッドは同じデータにアクセスしない、という方法が必要です。
そのやり方としてPV操作と言われる方法があります。
ポイント!PV操作
例えば、あるオブジェクトを利用したいとき、競合を防ぐには
P操作
オブジェクトがノンシグナル状態のとき : シグナル状態になるまで待つ
オブジェクトがシグナル状態のとき : ノンシグナル状態に変更行いたい操作(オブジェクトの利用)
V操作 : オブジェクトをシグナル状態にする
P操作を、wait動作とかオブジェクトをロックするなどとも言います。
また、V操作はsignal動作とかオブジェクトをアンロックするなどと言います。
ロック/アンロックというのが一般的な言葉でしょう。
また、オブジェクトのことをモニタと言います。
しかし、これだけでは意味が少し分かりにくいかもしれません。
ちょっと言い換えてみましょう。
スレッドを2台の車にたとえて、オブジェクトを道路、競合を衝突、シグナル状態を青信号、ノンシグナル状態を赤信号と変えてみます。
ポイント!PV操作
例えば、ある道路を利用したいとき、衝突を防ぐには
道路が赤信号のとき : 青信号になるまで待つ
道路が青信号のとき : 赤信号に変更道路の利用
道路を青信号にする
実は結構単純なことを言っています。
皆さんがこれを守れば衝突することはありません。
Javaのオブジェクトはこの信号機の機能があり、スレッドにはPV操作をするという機能があります。
あとは、この機能を使う、という指示をするだけです。
あるオブジェクトxに対してPV操作をしたい場合は、「synchronizedブロック」というものを実装します。
【synchronizedブロックの書式】
synchronized(x){ 行いたい処理 }
このように書くだけです。
【synchronizedブロックによる排他制御】
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 + "なので銀行に行きます"); //PV操作をする! synchronized(account){ //とってきたお金を所持金に加える 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円取引されました 残高は1000円です 現金が0円になりました お金が足りません 残高は1000円です 現金が-2000円になりました
前回と異なり、今度は残高が負になっていません。
片方のスレッドは取引処理ができず、cashが-2000のままです。これは要求仕様通りの結果です。
また、排他制御にはもう1つやり方があります。
「synchronizedメソッド」を利用する方法です。
synchronizedメソッド
synchronizedキーワードが指定されたメソッドは同時に複数のスレッドが実行できなくなります。
アクセス指定 synchronized 戻り値 メソッド名(引数リスト){ }
【synchronizedメソッドによる排他制御】
8行目のgetMoney ()メソッドに「synchronized」を追加します。
41、44行をコメントにします。
class BankAccount{ //銀行 private int balance; //通帳残高 public BankAccount(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 + "円です"); 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 + "なので銀行に行きます"); //PV操作をする! //synchronized(account){ //とってきたお金を所持金に加える 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円取引されました 残高は1000円です 現金が0円になりました お金が足りません 残高は1000円です 現金が-2000円になりました
プログラムの動きは同じです。
synchronizedブロックを使うときは、オブジェクトを扱うスレッドすべてに実装しなければいけません。
それでは大変ですので、オブジェクト側のメソッドをsynchronizedとします。
それにより、このメソッドが呼ばれるときは自動的にPV操作が行われます。
今まであったメソッドにsynchronizedというキーワードを付けるだけなのでとても楽です。
Java以外の言語ではPV操作を自分で実装しなければならないものもあります。
それを考えるととても便利なことが分かります。
マルチスレッドとデッドロック
今度は以下の例を考えてみましょう。
残高は0、husbandの現金は-2000です。
しかし、wifeが+3000持っています。
wifeが取り引きした後ならばhusbandも引き出すことができます。
husbandが最初に銀行口座にアクセスしたとき、まだwifeの取引が終わっていない可能性があります。
そのため、husbandは成功するまで取引を試みます。
単にwhile文を使う、というだけですがそのように書き換えてみましょう。
【デッドロック】
53行目のBankAccountの引数を「0」に修正します。(通帳残高を0にする)
55行目のwifeの現金を「3000」に修正します。
41行目から46行目のコメントを削除「synchronized」ブロックを復活します。
また「do~while文」を追加します。
8行目のgetMoney()からsychronizedを削除します。
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 + "なので銀行に行きます"); //PV操作をする! synchronized(account){ do { //とってきたお金を所持金に加える cash += account.getMoney(cash); } while(cash < 0); } System.out.println("現金が"+ cash + "円になりました"); } } public class InTheBank { public static void main(String[] args) { BankAccount account = new BankAccount(0); Person husband = new Person(account, -2000); Person wife = new Person(account, 3000); husband.start(); wife.start(); } }
実行結果
お金が足りません 残高は0円です お金が足りません 残高は0円です お金が足りません 残高は0円です お金が足りません 残高は0円です
Eclipseを使用している人はコンソールの「終了」ボタンをクリックしてください。
(ターミナルやコマンドプロントを使用している人は「Ctrl+C」を入力してください。)
大変なことになってしまいました。これはデッドロックという現象です。
デッドロックは、アプリケーションを作成する際に比較的起こしやすいエラーです。
ソフトを使っているときに動かなくなってしまった経験がないでしょうか?
それはデッドロックを起こしている可能性があります。
2つのスレッドがお互いの終了を待ち合う、というのがデッドロックです。
これは両方とも待ち状態になったまま復帰できません。
デッドロックは競合の反対の状態と考えられます。
競合はPV操作で回避できますが、デッドロックを100%回避する方法はありません。
「ソースコードを注意して読む」しかないのです。
このプログラムの場合は、スレッドhusbandの動作が変です。
残高が0なのに延々と口座をロックしたまま、というのは明らかに無意味な上、問題を助長しているだけです。
ロックしたままwhileループの操作に入り、抜けることがありません。
【デッドロックの回避】
do~while文をsynchronizedブロックの外にだします。
実行結果
現金が-2000なので銀行に行きます 現金が3000なので銀行に行きます お金が足りません 残高は0円です 3000円取引されました 現金が0円になりました -2000円取引されました 残高は1000円です 現金が0円になりました
今度は正しく動きました。
今回の実行結果では、最初にhusbandが処理を試みようとしますが、残高が0円なので処理を実行できません。
次にwifeが処理を行っています。
wifeは3000円を持っているので、講座に3000円を預金することになります。
この時点で残高は3000円ですので、husbandが処理を実行することができます。
しかしhusbandとwifeのどちらのスレッドが先に動くかはJavaVMが管理するので、毎回結果が異なる場合があり、husbandばかりが何度も処理を試みようとするかもしれません。
wifeが預金をするまでは、husbandの処理は失敗し続けます。
これでは無駄な処理が多くなる可能性があります。
それでは、wifeの処理が完了するまで、husbandには待機するようにスレッドに通知するにはどうしたらよいでしょうか。
それが次回説明する、「スレッドの同期制御」です。
「マルチスレッド Part5」へ続きます。
phoeducation.work
LINE公式アカウント
仕事が辛くてたまらない人生が、仕事が楽しくてたまらない人生に変わります。
【登録いただいた人全員に、無料キャリア相談プレゼント中!】