問題3-8
5人の大富豪が1つの募金箱に同時に募金していく様子をシミュレートしています。
大富豪を表現するMultiMillionaireクラスと、募金箱を表現するCollectionBoxクラスを作成してください。
また、これらのクラスを使用するThreadSafePracticeクラスのmainメソッドは完成しています。
(変更は禁止です)
【MultiMillionaireクラス】
Threadクラスを継承します。
募金箱インスタンスフィールドを定義します。
募金箱オブジェクトを引数に受け取るコンストラクタを定義します。
runメソッドをオーバーライドします。
for文を用いて100万回ループさせます。
for文の中では、募金箱に対して1円を募金します。
(1円募金×100万回ループなので、100万円寄付することになります)
【CollectionBoxクラス】
募金総額を表すインスタンスフィールドtotalAmout(int型)を定義します。
募金箱オブジェクトは、絶対に1つだけしか存在しないようにしてください。
・別クラスからインスタンス化できないようにコンストラクタを工夫してください。
・クラスメソッドgetInstanceを用いて、募金箱オブジェクトを取得してください。
・CollectionBox型のクラスフィールドを定義して、うまく利用してください。お金を寄付するためのcontributeメソッドを定義してください。
引数は寄付する金額(int型)で、戻り値はなしにします。
このメソッドをスレッドセーフにするように注意してください。募金総額を取得するためのgetTotalAmoutメソッドを定義してください。
引数はなしで、戻り値に募金総額フィールドの値を返してください。
ただし、最後に表示する募金総額には絶対にずれが生じないようにしてください。
【正しい実行結果】
募金総額は5000000円です。
【正しくない実行結果(不定)】
募金総額は3294875円です。
【ThreadSafePractice.java】
public class ThreadSafePractice { public static void main(String[] args) { // 募金箱オブジェクトの取得 CollectionBox cb = CollectionBox.getInstance(); // 偽物の募金箱を作れないようにする(コンパイルエラー) // CollectionBox imitation = new CollectionBox(); // 5人の大富豪オブジェクトを生成 MultiMillionaire mm1 = new MultiMillionaire(cb); MultiMillionaire mm2 = new MultiMillionaire(cb); MultiMillionaire mm3 = new MultiMillionaire(cb); MultiMillionaire mm4 = new MultiMillionaire(cb); MultiMillionaire mm5 = new MultiMillionaire(cb); // 募金の開始 mm1.start(); mm2.start(); mm3.start(); mm4.start(); mm5.start(); // 全員の募金が終わるまで待つ try { mm1.join(); mm2.join(); mm3.join(); mm4.join(); mm5.join(); } catch(InterruptedException e) { e.printStackTrace(); } // 募金総額の発表 System.out.println("募金総額は" + cb.getTotalAmout() + "円です。"); } } // ここに大富豪クラスを作成してください // ここに募金箱クラスを作成してください
解答例
【ThreadSafePractice.java】
public class ThreadSafePractice { public static void main(String[] args) { // 募金箱オブジェクトの取得 CollectionBox cb = CollectionBox.getInstance(); // 偽物の募金箱を作れないようにする(コンパイルエラー) // CollectionBox imitation = new CollectionBox(); // 5人の大富豪オブジェクトを生成 MultiMillionaire mm1 = new MultiMillionaire(cb); MultiMillionaire mm2 = new MultiMillionaire(cb); MultiMillionaire mm3 = new MultiMillionaire(cb); MultiMillionaire mm4 = new MultiMillionaire(cb); MultiMillionaire mm5 = new MultiMillionaire(cb); // 募金の開始 mm1.start(); mm2.start(); mm3.start(); mm4.start(); mm5.start(); // 全員の募金が終わるまで待つ try { mm1.join(); mm2.join(); mm3.join(); mm4.join(); mm5.join(); } catch(InterruptedException e) { e.printStackTrace(); } // 募金総額の発表 System.out.println("募金総額は" + cb.getTotalAmount() + "円です。"); } } // 大富豪クラス class MultiMillionaire extends Thread { // 募金箱インスタンスフィールド private CollectionBox cb; // コンストラクタ public MultiMillionaire(final CollectionBox cb) { this.cb = cb; } @Override public void run() { // 1円を100万回寄付する for(int i = 0; i < 1000000; i++) { cb.contribute(1); } } } // 募金箱クラス class CollectionBox { // 募金箱クラスフィールド private static CollectionBox cb; // 募金総額 private int totalAmount; // privateコンストラクタ // (外部からのインスタンス化を禁止するため) private CollectionBox() {} // 募金箱インスタンスの取得 public static CollectionBox getInstance() { // インスタンスがまだなければ生成 if(cb == null) { cb = new CollectionBox(); } return cb; } // 寄付メソッド public synchronized void contribute(int money) { totalAmount += money; } // 募金総額の取得メソッド public int getTotalAmount() { return totalAmount; } }
解説
「マルチスレッド」を行うプログラムをコーディングする際に、必ず意識しなければいけないのが「スレッドセーフ」です。
「スレッドセーフ」とは、「スレッド的に安全である」ということです。
では、その逆で「スレッド的に危険」とは何を指すのでしょうか。
簡単に言えば「データの欠落」です。
システムはデータの正しさがすべてです。
そこをしっかり意識しましょう。
今回の問題では、大切なテーマが2つあります。
1つ目のテーマが「募金箱オブジェクトは、絶対に1つだけしか存在しないようにしてください」という文章を実現する仕組みです。
まず、外部から簡単にインスタンス化できないように、コンストラクタのアクセス修飾子を「private」に設定します。
しかし、次のようなコーディングをしてはいけません。
// 募金箱クラス class CollectionBox { // privateコンストラクタ private CollectionBox() {} // 募金箱インスタンスの取得 public static CollectionBox getInstance() { return new CollectionBox(); } }
これだと、getInstanceメソッドを呼び出すたびに異なる募金箱オブジェクトをインスタンス化します。
やりたいことは、何度getInstanceメソッドを呼び出しても、同一の募金箱オブジェクトを返すことです。
そんなことができるのでしょうか。
実は良い方法があるんです。
問題文にもヒントがありましたが「CollectionBox型のクラスフィールドを定義」します。
クラスフィールドは、たった1つしか存在しませんから、ちょうど良いわけです。
もちろん、最初はCollectionBox型のクラスフィールドにはnullが格納されていますから、そこだけ次のようにうまく調整します。
// 募金箱クラス class CollectionBox { // 募金箱クラスフィールド private static CollectionBox cb; // privateコンストラクタ private CollectionBox() {} // 募金箱インスタンスの取得 public static CollectionBox getInstance() { // インスタンスがまだなければ生成 if(cb == null) { cb = new CollectionBox(); } return cb; } }
この技法は「シングルトン」と呼ばれるデザインパターンの1つです。
これは覚えておいて損はないテクニックですので、ぜひマスターしておきましょう。
2つ目のテーマは「スレッドセーフ」です。
今回は5人の大富豪が1円を100万回寄付しているので「募金総額は5000000円です。」が出力されるはずです。
しかし、そうならなかった人もいたのではないでしょうか。
今回の処理をプログラム的に説明すると、「5つの大富豪スレッドが、たった1つの募金箱オブジェクトの募金メソッドをほぼ同時に100万回ずつ実行している」わけです。
複数のスレッドが同時にCollectionBoxオブジェクトのcontributeメソッドを実行すると、データが壊れてしまうわけです。
これがマルチスレッドで一番気をつけないといけない危険性「データの欠落」です。
解決方法は次のように考えます。
「ひとりの大富豪が募金箱に寄付(つまりメソッド実行)している間は、他の大富豪(つまり他のスレッド)はそれが終わるまで待ち続ける」
これをプログラム的に実現するだけです。
Javaでは、すべてのオブジェクトに、たった一つだけ「ロックフラグ」というものがあると考えられます。
一つのスレッドが「ロックフラグ」を取得すると、そのオブジェクトには鍵が掛かって他のスレッドは待たなくてはいけなくなるのです。
「ロックフラグ」を取得したスレッドがそれをオブジェクトの返す、つまり鍵を解除すれば待っていたスレッドが今度はそれを取得して鍵をかけるわけです。
少し難しそうな話ですが、これを実現するのは「synchronized」キーワード1つです。
このキーワードの最も基本的な使用法は、メソッド定義の先頭に付加するだけです。
// 寄付メソッド public synchronized void contribute(int money) { totalAmount += money; }
こうすると、一人の大富豪スレッドがcontributeメソッドを実行する直前に募金箱オブジェクトがたった一つだけ持つ「ロックフラグ」を取得し、募金箱に鍵をかけるわけです。
その間に、他の大富豪がcontributeメソッドを実行しようとしても、鍵がかかっているためにメソッドの中に入れず、鍵が開くのを待つことになります。
「待つ」のであって「諦める」わけではないのがポイントです。
このsynchronizedキーワードによるデータの欠落の防止策を「排他制御」といい、マルチスレッドでスレッドセーフを実現するにはかかせない技術です。
しかし、だからといって、なんでもかんでもメソッドにsynchronizedキーワードをつければ良いというものではありません。
パフォーマンスが悪化しますし「デッドロック」というさらなる危険性を招くおそれがあるからです。
最後に補足ですが、「ロックフラグ」はsynchronizedキーワードが付加されたメソッドだけで有効なのを知っておいてください。
たとえ、ある大富豪がsynchronizedキーワードの付加されたcontributeメソッドを実行中でも、他の大富豪はsynchronizedキーワードのついていないgetTotalamoutメソッドなどは関係なく実行できます。
参考図書
LINE公式アカウント
仕事が辛くてたまらない人生が、仕事が楽しくてたまらない人生に変わります。
【登録いただいた人全員に、無料キャリア相談プレゼント中!】