概要
以前マルチスレッド環境における「java.util.HashMap」に対し、複数のアプリがアクセスした際、応答が帰ってこなかっり、HashMapの値が入れ替るなどの事象がありました。
その原因や対応など備忘録的に以下を記載しようと思います。
原因
とても単純で、「java.util.HashMap」は複数のスレッドから同時にputやgetが実行されると何が応答されるか保障されないことを理解していなかったり、排他制御をしないで使用していたからです。
この事象について調べてみると、昔からいろいろなサイトに記載されていることがわかります。
■何が応答されるかわからない
どうやらHashMap.putを実行すると、HashMap内のデータ構造が再構築されるみたいです。
この最中にgetが実行されると、指定されたキー情報を元に対象を検索しに行きますが、再構築中のため延々と検索したりして何を応答するのか予想がつかないみたいです。
これは、よく言われるputとgetが同時実行された際に起こる、無限ループやデータ構造が破壊される事象です。
■HashMapの値が入れ替る
これはスレッドローカル変数に対してではなく、static変数に対してputやgetをしており、排他制御を実装していないことが原因でした。
サンプルですが以下のようなソースです。
開発環境で以下の手順でデバッグ実行してみるとわかりますが、スレッド1つ目が25行目でmapにput後、スレッド2つ目が28行目でputしているため、スレッド1つ目がgetした場合、違う値に変更されています。
【手順】
1.24行目でブレークポイント
2.スレッド1つ目を25行目まで実行
3.スレッド2つ目を29行目まで実行
4.スレッド1つ目を26行目まで実行
これがシングルスレッドなら問題ないですが、複数スレッドからアクセスする場合への考慮が足りていなかったです。
処理順番のイメージは以下です。
対応
対応は、各事象ごとに考えます。
■何が応答されるかわからない
これは、HashMapに対してgetとputによる同時アクセスが原因となりますので、同時アクセスに対して同期を取るようにして、誰かがアクセスしている最中はもう片方からのアクセスを待たせる対応となります。
・synchronizedメソッド
メソッドに対して排他制御をかけます。
public synchronized static void putMap(String[] args){
// ~何かしらの処理~
}
誰かがこのメソッドを使用している場合、他からアクセスしてきた人は最初にアクセスした人の処理(synchronizedの範囲内)が終了するまで待たせる状態となります。
しかし、上記のサンプルはstaticメソッドなのでマルチスレッド環境化でも有効ですが、staticではないメソッドの場合、そのインスタンス単位の範囲となるため、スレッド間に対する排他制御は有効となりませんので注意が必要です。
・java.util.concurrent.ConcurrentHashMap
HashMapをjdk1.5から使用可能となるConcurrentHashMapに変更します。
private static ConcurrentHashMap<String,String> map = new ConcurrentHashMap<String,String>();
HashMapはスレッドセーフではないですが、ConcurrentHashMapはスレッドセーフとなり同期化も勝手に実施してくれます。
自前でsynchronizedを実装しなくて済みますし、パフォーマンスも向上します。
なので、複数スレッドからアクセスが頻繁に発生する場合は、ConcurrentHashMapを使用したほうがいいです。
■HashMapの値が入れ替る
複数スレッドからstatic変数にアクセスしているので、タイミングによって値が変更されるのは当然でした。
自分がputした値が変更されたくないのであれば、排他制御を実施したり、スレッドローカルの変数にオブジェクトをコピー(clone)して使用するなどの対応が必要です。
まとめ
淡々とソースコードを記述していると気づきにくいですが、どのような環境化で実行されるプログラムなのか意識しながら記述しないと、後々の調査などが大変です。
なので、スレッドセーフなのか、頻繁に変更してもいいものか、誰が利用するべきリソースなのかなど常に意識しながら記述したほうがいいと思います。