Mapの同期化の続き
というわけで以下のような適当なプログラムを・・・
サンプルでは各Map実装の挙動を見るための修正箇所を絞りたかったので
こんな感じですが、
Mapインターフェースで受けない方が、
ConcurrentHashMapにあるアトミックなメソッドが使えて便利なケースが多いです。
putIfAbsentとか。
また、同期されるという点のアピールでもMapで受けない方が可読性がよいかも。
ConcurrentMapインターフェースで受けてもいいけど、実装クラスが限定されているので。。。
package collection.map; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; public class MapThreadTest { public static Map<String, String> hashMap; public static Map<String, String> syncMap; public static Map<String, String> concurrentMap; public static final int SIZE_MAP = 50000; static{ Map<String, String> map = new HashMap<String, String>(); for (int i = 0; i < SIZE_MAP; i++) { String v = String.valueOf(i); map.put(v, v + "Value"); } hashMap = new HashMap<String, String>(map); syncMap = Collections.synchronizedMap(map); concurrentMap = new ConcurrentHashMap<String, String>(map); } public static void main(String[] args) { new Thread(new MapPrinter()).start(); new Thread(new MapDeleter()).start(); } /** * テスト対象Mapを変更する。 * @return */ public static Map<String, String> getTargetMap() { return concurrentMap; } /** * Mapのdeleteを行う。 * */ private static class MapDeleter implements Runnable { public void run() { try { //0.5秒ほど待機 TimeUnit.MILLISECONDS.sleep(500); Map<String, String> map = getTargetMap(); map.clear(); System.out.println("DELETETHREAD>##################################"); System.out.println("DELETETHREAD>############# clear #############"); System.out.println("DELETETHREAD>##################################"); } catch (Exception e) { e.printStackTrace(); } } } /** * Mapの内容を表示していく * */ private static class MapPrinter implements Runnable { public void run() { try { Map<String, String> map = getTargetMap(); Set<String> keys = map.keySet(); System.out.println("Mapの件数:" + keys.size()); int i = 0; for (Iterator iter = keys.iterator(); iter.hasNext();) { String key = (String) iter.next(); String v = map.get(key); TimeUnit.MILLISECONDS.sleep(1); i++; } System.out.println(i + "件処理対象"); } catch (Exception e) { e.printStackTrace(); } } } }
ConcurrentHashMapの場合、ゆるやかに終わる。
Mapの件数:50000 DELETETHREAD>################################## DELETETHREAD>############# clear ############# DELETETHREAD>################################## 229件処理対象
ゆるやかにおわるってーとあれだけど、行けるところまで行って終わる。か。
HashMap,同期化Mapの場合
Mapの件数:50000 DELETETHREAD>################################## DELETETHREAD>############# clear ############# DELETETHREAD>################################## java.util.ConcurrentModificationException at java.util.HashMap$HashIterator.nextEntry(HashMap.java:841) at java.util.HashMap$KeyIterator.next(HashMap.java:877) at collection.map.MapThreadTest$MapPrinter.run(MapThreadTest.java:77) at java.lang.Thread.run(Thread.java:595)
例外となる。
Concurrentでも例外はあるので注意
Concurrentでも、
String v = map.get(key).toLowerCase();
こんな感じだと、NullPointerExceptionが発生するので注意。
※keyは取得できるけど、get時に実際のMapが既に空っぽになっているので。
厭なら、entrySet()を使ったりすればよい。
結局
じゃぁ、結局はちゃんと排他を考えてやる必要があって、
削除にて
synchronized (map) { map.clear(); }
表示にて
synchronized (map) { Set<String> keys = map.keySet(); System.out.println("Mapの件数:" + keys.size()); int i = 0; for (Iterator iter = keys.iterator(); iter.hasNext();) { String key = (String) iter.next(); String v = map.get(key); TimeUnit.MILLISECONDS.sleep(1); i++; } System.out.println(i + "件処理対象"); }
と同じMapをロックするしかないというわけですね。
まぁ、忘れが起こらないように、ロック箇所をあまり意識させないような作りにする必要あるんだろうけど。
特に更新系の処理。
ちなみに、HashMapの場合、削除のところでsynchronizedブロックを忘れると、
普通にmapにアクセスできるのは忘れずに。意外に忘れがち。
Collections.synchronizedMap(map)があるじゃない
このあたりで、間違いの原因になるだけ?と思われがちだった
Collections.synchronizedMap(map)
が効いてきます。
この場合、Mapの各処理が同期されるので、map.clear()呼出に関しては、
ロックが取得できないので「待ち」となる。
なので、map.clear()のところのブロックは不要となる。
ちなみに同期化を取ることにより、劇的に遅くなる。
では、ここで適切な個所だけ同期されるというConcurrentHashMap
の出番となるわけだけど、この場合、map.clear()
の処理に関して、ブロックをどうするか?というのは方針次第となる。
※ちなみに、この例の場合、clearを同期化するなら、Concurrentである必要はあまりない。
別のロックなし取得系があるなら別かもしれないけど。
また、clearを同期化しないなら、取得系のところもロックは不要じゃないかな。
※ConcurrentHashMapは内部のセグメントに対してロックを行っているので、
MapそのものをロックするのであればConcurrentHashMapである必要はないのでは?ということ。
同期をとる場合は、Mapがすべて変更前であることが保証されるし
同期をとらない場合は、とれるところまでとった状態となる(valueがnullだったりすることはあるけど)
たとえば、マスタをキャッシュしておいた場合で、更新が入った場合、
常にキャッシュの状態を見るのか、最新の状態がほしいのか?による。
同期をとった場合、プログラム的に例外はなくても、実際には存在しない値などを利用してしまうことが考えられる。
というのは、そもそも同期化を取るべきなのか、例外を出してハンドリングするべきなのか?
という話にも繋がるのでConcurrentHashMapだけの話ではなかったり。
ただ、まぁ、そもそも最新が必要なら、キャッシュ的な共有Mapなんてつかっちゃだめだよね。
だめではないんだけどいろいろ難しいよね。
以下は必読
ConcurrentHashMapについては
http://www.itarchitect.jp/technology_and_programming/-/24161-1.html
に詳しい。
とまぁぐだぐだ書いてきたけど、
一応の挙動を見ているだけだし、
APIドキュメントに書いてあることも結構難しいので、私の解釈も間違っている可能性があります。
ので、あまり信じないでください。だめパターンとかもあるかもしれないし。
ただ一応自分のメモということで。