*

Javaによる非同期処理入門その1[非同期処理の実装方法の概説]

公開日: : 最終更新日:2016/12/05 Java ,


スポンサードリンク



Javaによる非同期処理に関するエントリーを前々から作成したいと思っていたのですが、ついに作成してみました。
と言っても、5,6回を予定しておりますのでこれからなのですが・・・

第一回目は、非同期処理の実装方法の概説となります。

1.非同期処理が必要な理由

最近のアプリケーションは、画面操作後のエフェクトが綺麗ですよね、
特にWeb2.0以降(古っ)のWebアプリケーションではボタンクリック後の処理の非同期化はごく当たり前です。
Webアプリケーション以外でも、処理が非同期(バックグラウンド)で動作するのは一般的ですよね。

では、この非同期処理が存在しないと、もしくは非同期処を利用していないとどうなるでしょうか?
答えは簡単です。「ボタンをクリックした後に待たされる時間が増える」です。

アプリが固まったように反応しなくなるあれです。Webアプリケーションでボタンの排他を行っていない場合は、続けてクリックすると処理が失敗します。

ユーザビリティを担保するためには、非同期処理は必要不可欠な物と言えます。

と、利用者の観点からはそうなんですが、実装者側からすると非同期って面倒な物なんですよね・・・
ひとたびバグると問題点の絞り込みが難しいのです。

当然ですが、綺麗なコードを書いていて、かつデバッグしやすいような設計であるとその労力は格段に削減されます。

これも何年か前から言われているのですが、非同期処理なんて自動生成ツールが発達して、簡単に実現できるようになる。
みたいな・・・、ツールの名前も忘れちゃいましたが、なかなかそんなに世の中うまく行かないですね。

とは言え、最近ではiOS用の非同期フレームワークが熱いですね。
BrightFutures」はかなり便利ですよね
Future&Promiseパターンを用いたSwift用の非同期フレームワークです。

Javaでしたら
Bolts

JavaScriptでしたら
Promise

なんかが有名だと言えます。

イマイチ話しが発散してしまっていますが、非同期処理を安全に実現するためにはノウハウが必要だと言えます。
BrightFuturesのようなフレームワークの利用を前提にしても、非同期処理や排他処理に対するある程度の知識は必要ですし、知識が多い方がトラブりにくいです。
(そんなのあたりまえですが・・・)

2.実装する処理の内容説明

どんな内容が分かりやすいか色々考えてみたのですが、
複数人がお金を奪い合うゼロサムゲームで行きたいと思います。
(一人が一スレッドに相当し、実際のゲームではないです。)

・一人毎の所持金、ゲームに参加する人数とプレイヤーそれぞれの名前を入力
・一回で奪える金額は100、200,300,400,500円でどの金額になるかはランダムに決定
・一人以外のプレイヤーの所持金が0になった時点で終了
・各プレイヤーの動作の回数は5回で固定

3.実際に実装してみる

何も考えずに実装してみる

まじめに実装すると、排他処理が必要になりますが、まずは何も考えずに実装してみます。

また、非同期処理(マルチスレッド)の実現方法にも何種類かありますが、今回の内容は概説としていますので一番安易な方法のjava.lang.Threadクラスを利用する方法とします。

少し情報を追加すると
ThreadクラスはRunnableインターフェースを実装したクラスで、Threadを継承したクラスを作成し、runメソッドにスレッドで処理したい内容を記載し、実装したクラスのインスタンスを作成後にstartメソッドを呼び出せばスレッドが起動します。

runメソッドが終了すればスレッドも終了します。

スレッドの終了待ちを行うには、実装したクラスのインスタンスのjoinメソッドを呼び出すことで行えます。

初回実装内容

Personクラス

Personクラスが各プレイヤーのスレッドに対応するクラスとなります。
メンバー変数にmoney(自分の持っているお金の金額)とplayerNameと対戦相手のリストを含んでいます。
実際のスレッドの処理はrunメソッド内に記載しています。

ゲッター、セッターが必要なので少し長くなりましたが、処理自体は簡単だと思います。
ダラダラ記載していて、綺麗とは言えないですがこの方が例として見やすいと思います。

ZeroSumGameクラス

Personクラスの作成とスレッドの起動、スレッドの終了待ちを行います。

上記の例では、プレイヤーは3人で、スタート時の所持金は100円としています。

実行結果

実行すると以下のような結果となりました。

予想通りにツッコミどころ満載の結果ですね!
まず13行目です。このタイミングで花子の残金が0になったので次郎は花子はゲームオーバーと見なしています。
しかし14行目で花子は自分がゲームオーバーとなったことが認識できておらずゲームを続行してしまっています。
17行目でも花子がゲームを続行していることが見てとれます。

同様に14行目で太郎の残金が0になったと花子が認識していますが、その後のも太郎はゲームを続けています。

問題の根源は
1.自分の残金が0になったことを判定するロジックの実行タイミング
2.他のプレイヤーの残金が0になったと認識するタイミング
3.各プレイヤーの残金に排他がかかっていないこと
にあると言えます。

排他処理を実現し、自分と対戦相手の残金の値が保証された状態での処理とする必要があります。

二回目の実装内容

Personクラス

Personクラスに排他処理用のlockを追加し、
runメソッドの開始時点で自分のlockを取得→synchronized (lock)
target(お金を奪おうとしている他のプレイヤー)のロックを取得→synchronized (target.lock)
のブロックを追加しました。

synchronized(排他処理用のオブジェクト)
で処理を囲むことで、排他処理用のオブジェクトのロックを獲得できたスレッドのみがsynchronizedブロックの処理を実行できます。
排他処理用のオブジェクトのロックを獲得できたなかったスレッドは、ロックを獲得しているスレッドの終了を待ち、終了すると処理が実行されます。
複数のスレッドがロックの獲得待ちを行っている場合は、待ち状態になったスレッドのどれか1つが実行されます。

これで整合性が取れるはずです。

実行結果

実行結果は以下のようになりました。

行の始めに表示されているのはスレッドIDとなります。
上記の場合はスレッド10が太郎、スレッド11が次郎、スレッド12が花子のスレッドです。

予想通りデッドロックが発生しています。
各プレイヤーは自分のlockを獲得し、お金を奪おうとしているプレイヤーのlockの獲得を行いますが、奪われようとしているプレイヤーも自分のlockを獲得しています。

lockの獲得要求は、そのlockが開放されるまで待ち、開放されるとロックを獲得できsynchronizedで囲まれたブロックの処理が実行されるとなります。
太郎も次郎も花子も自分のlockを獲得して、他の誰か一人のlockを要求していますが、誰も自分のlockを開放する処理が存在していないのでデッドロックとなってしまいます。

何度も実行すれば、奇跡的にデッドロックが発生しない実行結果を得る事ができる可能性もありますが、そんなプログラムなんて意味ないですので、この問題点に対処します。

三回目の実装内容

Personクラス

java.util.concurrent.locks.StampedLockクラスのtryWriteLockを利用しデッドロックを解消してみます。

正確には、デッドロックは一時的には発生しますが、ロックの取得要求がタイムアウトしリトライが行われるようにしてみます。

見にくくなっていますので、先にrunメソッドをリファクタリングしてみます。

うんうん、かなり見やすくなりました。
よく忘れがちな事ですが、処理を改造する前にリファクタリングを行う事って凄く重要です。
リファクタリングと処理の改造をいっしょに行うと、どちらの作業によるミスか判別がつきません。

実行結果もオッケーですね!
(おいおい、てデッドロック発生してるのでオッケーではないですよ・・・)

引き続きロック獲得に失敗した時のリトライ処理を施したコードとなります。

・7,8行目のメンバー追加
TIMEOUTはロック獲得を待ち合わせる秒数となります。
8行目はjava.util.concurrent.locks.StampedLock型の変数宣言です。修正前はObject型の変数でした。

・runにtry catchを追加
38行目から66行目です。finallyでロックが獲得できていればロックを開放するとの処理がポイントとなります。

・runの排他処理をStampedLockのtryWriteLockに変更
StampedLockではReadLockとWriteLockが利用できます。
ReadLockは文字通り読み取り専用の排他(値の参照のみ)
WriteLockは書き込みも出来る排他(値の参照と更新)
となります。

ReadLockは複数のスレッドが獲得することができ、WriteLockは単一のスレッドのみ獲得できます。

41行目の処理

でタイムアウト5秒でWriteLockの取得を要求し

42行目でWriteLockの取得が成功した場合に後続の処理を行うとの内容となります。
(tryWriteLockはWriteLockの獲得に失敗すると0を返却します。)

・getMoneyFromTargetの排他処理をStampedLockのtryWriteLockに変更
runメソッドと同様の変更なので説明は省略させていただきます。

実行結果

ロックの取得要求がタイムアウトした時にリトライできるようにはなりました。

でもやっぱりダメですよね・・・、実質的に無限ループになってます。
確かにこの修正でゲームが動作する可能性は高まりましたが、まだまだあり得ない確率ですよね。

何が問題なのでしょうか?

そうそうです!!、結局ロックを2つ獲得しようとしている事に無理があるのです!!!

四回目の実装内容

デッドロックの解消には以下の2点の考慮が必要となります。
・ロックの獲得と開放をこまめに行う。
・同時に2つのロックの獲得は行わない。

リファクタリング

まずは、ロックの開放処理を別メソッドにします。

デッドロックの解消

修正対象はrunメソッドとgetMoneyFromTargetメソッドとなります。

・10行目
一回目の自分のlockの獲得を行っています。コード的には変更されていないのですが、動作的には一回目のロックと変わっています。
自分の残金が0かどうかを判定するためには、排他を行う必要がありますよね。そのためのロック獲得となります。

・20行目
自分の残金が0かどうかを判定するためのロックの開放を行います。同様に排他処理の開放時に利用するmyLockStampも初期化しています。これを初期化しないと獲得できていないロックの開放を行ってしまうからです。

・30行目
getMoneyFromTargetメソッドの戻り値がintに変更されたことによる修正です。

修正前は、targetから奪取したお金はgetMoneyFromTargetメソッド内で自分の残金に反映していました。
しかし、その反映処理はgetMoneyFromTargetで実施しなくてもOKですよね。

と言うか、今回の修正で自分のlockは排他されていなくっており、このタイミングで残金に反映してしまってはダメです。

targetの残金への反映はgetMoneyFromTargetメソッド、自分の残金への反映はrunメソッドで行うように変更しています。
自分の残金への反映部分は33行目以降となります。

・33行目
二回目の自分のlockの獲得を行っています。この目的は自分の残金への反映を行うためです。

・38行目
自分の残金への反映処理です。

実行結果

修正後の実行結果は以下のようになりました。

少し長いですが、うまくいって・・・ませんね。花子が暴走しています。
本来は審判がやるべきゲームオーバーの反映を各プレイヤーで行っていることが根本的な問題ですが、そこまでやると話がややこしくなりますので、getMoneyFromTargetを実行する前にも自分の残金が0になっていないかのチェックを追加します。

実行すると、うーんやはりチェックのタイミングがすくな過ぎますね。
残金が0になっているのに他のプレイヤーからお金を奪ってしまっています。

先ほど追加したチェックロジックを取り除き、isGameoverで自分のロック(ReadLock)を取得するように変更し、isGameoverを呼び出す回数を増やします。
当然ですが、getMoneyFromTargetを呼び出した後でもisGameoverを呼び出す必要があります。
このタイミングでisGameoverがtrueを返すとgetMoneyFromTargetで奪った金額をtargetに戻さないといけないです。

五回目の実装内容

isGameoverで行うロック獲得後の処理は更新は行わないでReadLockとしています。

実行すると以下のようにうまくいきました。

これで全てうまくいっているかと言うとそうでもないです。
以下の結果がうまくいかないパターンとなります。

まあ、相打ちって感じで、ルールとしてこれをありとするのであれば問題ないですし、なしであればさらなる排他と同期を考えないといけないですが、今回はありで・・・

「Javaによる非同期処理入門その1[非同期処理の実装方法の概説]」は以上です。


スポンサードリンク



関連記事

JUnit入門その2[Eclipse4.4のJUnitプラグインのassertThatの使い方]

JUnit入門その1ではEclipseのJUnitプラグインの基本的な使い方を説明させていただきまし

記事を読む

Eclipseのインストールと日本語化とJDK8(Java8)対応[Eclipse4.4とEclipse4.3]

インストールするEclipseのバージョンですが、とりあえず4.3をターゲットとしておき、 4.4

記事を読む

java8(JDK8)の新機能をEclipseとJUnitで[インターフェースのデフォルト実装の使い方]

本エントリーでは、まずSAM Typeの説明をさせていただきます。 その流れの中で「java8の新

記事を読む

Java8のラムダ式とStream APIを利用してコーディング量の削減サンプル集

Java8になりラムダ式と「Stream API」が利用できるようになりました。 C#では一足早く

記事を読む

Eclipse4.3のチュートリアル機能で”Hello World”アプリケーションの作成方法を説明する。

Eclipseのインストールが終了したので、各画面エリアの名称の説明、各画面エリアの使い方の説明を

記事を読む

Java8の新機能について

オラクルは2014年3月18日(日本時間3月19日早朝)に「Java 8」を正式に公開しました。

記事を読む

Java超入門 with Eclipse[3:クラスに関する基礎知識(クラスとインスタンスとパッケージ)]

Javaといえば、「オブジェクト指向」とのイメージがとっても強いですよね。 そう、そうです。間違い

記事を読む

JUnit入門その1[Eclipse4.4のJUnitプラグインの基本的な使い方]

利用する環境の作成につきましては、「Eclipseの使い方(Windows環境のEclipse4.3

記事を読む

Eclipse4.4,4.3の使い方[エディタのフォントサイズの変更方法]

今回は、Eclipse4.4と,4.3におけるエディタエリアのフォントサイズの変更方法を説明させてい

記事を読む

Eclipseの使い方(Windows環境のEclipse4.3、Eclipse4.4)

Eclipse4.4.0よりJDK8を正式サポートするそうです。 Eclipseトップレベルプ

記事を読む

Message

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

Spring5入門[STS(Spring Tool Suite)で簡単なWebアプリの典型的なユニットテストの実現方法]

前回は「Spring入門」で、Spring MVCを利用した簡単なWe

Spring5入門[STS(Spring Tool Suite)の環境作成と簡単なWebアプリの作成]

Struts1ももう過去の遺物になり、SAStrutsもEOLとなりも

Selenium入門その6[Selenium3でWebDriver(Java/Junit4)の環境を作成しEdge,Chrome,Firefoxで確認してみる]

Selenium3も3.0.1がリリースされましたし、今後は本格的にS

Selenium利用時のトラブルシューティング方法[クリック編]

Seleniumは便利なテスト自動化ツールですし、今後は更なる利用者の

Java8のラムダ式とStream APIを利用してコーディング量の削減サンプル集

Java8になりラムダ式と「Stream API」が利用できるようにな

→もっと見る

Optimization WordPress Plugins & Solutions by W3 EDGE
PAGE TOP ↑