2015

Java 8 時代の ConcurrentHashMap で遊んでみた。

Java

またまた Blog の更新をさぼっています。

と言うことで適当なエントリーを書いてみます。

ConcurrentHashMap

J2SE5.0 で java.util.concurrent に ConcurrentHashMap が導入されました。

ConcurrentHashMap は容易に安全に並行処理を実現するために作られました。

Collections.synchronizedMap ってのもあるのですが、それは使い方が微妙で詳しい説明は省略しますが Iterator などの反復処理で ConcurrentModificationException をスローすることがあります。

Collections.synchronizedMap は、Map のロックを取得すると他のスレッドからアクセスを禁じられます。

ConcurrentHashMap だと、Iterator および Enumeration は、ある時点または反復子/列挙の作成以降のハッシュテーブルの状態を反映する要素を返すように設計されているため、

ConcurrentModificationException をスローすることはありません。

しかし、一度に 1 つのスレッドだけしか反復子を使用できません。

あと、HashMap はエントリーのキーや値にnullを使用できますが、ConcurrentHashMapでは使用できません。

NullPointerException が投げられます。

ConcurrentHashMap は、HashMap のようなハッシュを使った Map ですがロックストライピングと言われる粒度の小さなロック方式を採用しています。

つまり、Map 全体をロックして排他アクセスする必要がなければ  ConcurrentHashMap は、とても魅力的な存在となります。

しかし、J2SE5.0 の時には、ConcurrentHashMap を便利に使うにはまだまだパーツが揃っていませんでした。

便利に使えそうだったのはアトミックに実行される下記メソッドくらいでした。

public V putIfAbsent(K key, V value)

public boolean remove(Object key, Object value)

public V replace(K key, V value)

public boolean replace(K key, V oldValue, V newValue)

また古い J2SE5.0 ネタかと思わせて、ここから最新の Java SE 8 ネタです。

Java SE 8 で ConcurrentHashMap に追加された便利な機能をいくつか試してみます。

はじめに、ConcurrentHashMap というネーミングから何をやってもスレッドセーフだと思い込んでしまうと泣きます。

例えば、下記プログラムのように複数スレッドから ConcurrentHashMap の値の更新があるとします。

最終的に Key “HOGE”, Value は 80 となることを期待しますが残念なことになります。

このプログラムの実行結果は次のようになります。

ForkJoinPool.commonPool-worker-18: HOGE, 48
ForkJoinPool.commonPool-worker-29: HOGE, 58
ForkJoinPool.commonPool-worker-8: HOGE, 65
ForkJoinPool.commonPool-worker-4: HOGE, 67
ForkJoinPool.commonPool-worker-11: HOGE, 68
ForkJoinPool.commonPool-worker-15: HOGE, 70
ForkJoinPool.commonPool-worker-25: HOGE, 73
ForkJoinPool.commonPool-worker-22: HOGE, 75
main : HOGE, 75

更新処理まではスレッドセーフでないということですね。

それではどのように対処すればいいでしょうか?

java.util.concurrent.atomic.LongAdder を使ってみます。

LongAdder クラスは、プリミティブ型に対するアトミック操作を行うため Java SE 8 から追加されたクラスです。

LongAdder クラスには、add(1)と等価な increment() メソッドと、add(-1)と等価な decrement() メソッドがあります。

LongAdder クラスを使って修正したプログラムは下記のようになります。

このプログラムの実行結果は次のようになります。

ForkJoinPool.commonPool-worker-25: HOGE, 49
ForkJoinPool.commonPool-worker-18: HOGE, 63
ForkJoinPool.commonPool-worker-11: HOGE, 71
ForkJoinPool.commonPool-worker-4: HOGE, 72
ForkJoinPool.commonPool-worker-29: HOGE, 73
ForkJoinPool.commonPool-worker-8: HOGE, 78
ForkJoinPool.commonPool-worker-15: HOGE, 79
ForkJoinPool.commonPool-worker-22: HOGE, 80
main : HOGE, 80

期待通りにうごいてくれました。

でも、ConcurrentHashMap<String, LongAdder> とかに変更するのも面倒ですよね。

それでは、Java SE 8 でのもっと素敵な修正プログラムを紹介します。

下記のようなアトミック操作をするメソッドを利用します。

public V compute(K key, BiFunction<? super K,? super V,? extends V> remappingFunction)

public V computeIfPresent(K key, BiFunction<? super K,? super V,? extends V> remappingFunction)

public V merge(K key, V value, BiFunction<? super V,? super V,? extends V> remappingFunction)

compute メソッドは、指定されたキーと現在マップされている値に対するマッピングの計算を試みます(現在のマッピングが存在しない場合はnull)。

computeIfPresent メソッドは、指定されたキーの値が存在する場合、キーと現在マップされている値から新しいマッピングの計算を試みます。

merge メソッドは、指定されたキーがまだ(nullでない)値と関連付けられていない場合は、指定された値に関連付けます。それ以外の場合は、指定された再マッピング関数の結果で値を置換し、nullの場合は削除します。

今回は、computeIfPresent メソッドを使ってみました。

他のメソッドの使用例はコメントアウトして記述してあります。

プログラムは下記のようになります。

このプログラムの実行結果は次のようになります。

ForkJoinPool.commonPool-worker-4: HOGE, 59
ForkJoinPool.commonPool-worker-15: HOGE, 61
ForkJoinPool.commonPool-worker-18: HOGE, 62
ForkJoinPool.commonPool-worker-22: HOGE, 68
ForkJoinPool.commonPool-worker-11: HOGE, 74
ForkJoinPool.commonPool-worker-25: HOGE, 78
ForkJoinPool.commonPool-worker-8: HOGE, 79
ForkJoinPool.commonPool-worker-29: HOGE, 80
main : HOGE, 80

これまでのところは J2SE5.0 でも注意が必要で replace メソッドを使う方法、AtomicLong を使うなどの方法がありました。

Java SE 8 では綺麗にその問題を解決できるようになりましたね!

それでは、これから Java SE 8 で追加されたスレッドセーフなバルクオペレーションを試してみます。

下記のプログラムを組んで、Java SE 8 で ConcurrentHashMap に追加された機能の一部を試してみました。

このプログラムの実行結果は次のようになります。

CPU: 32
ForkJoinPoolParallelism: 7

マッピング数: 18

(○・ω・)ノ————- forEach (BiConsumer) ————-
main: JDK 1.1.7 = Brutus
main: Java SE 9 =
main: JDK 1.1.8 = Chelsea
main: Java SE 8 = null
main: JDK 1.1.5 = Pumpkin
main: JDK 1.1.6 = Abigail
main: J2SE 1.2 = Playground
main: JDK 1.1.4 = Sparkler
ForkJoinPool.commonPool-worker-3: J2SE 1.2.2 = Cricket
main: Java SE 7 = Dolphin
ForkJoinPool.commonPool-worker-3: J2SE 1.3.1 = Ladybird
ForkJoinPool.commonPool-worker-4: J2SE 1.4 = Merlin
ForkJoinPool.commonPool-worker-1: J2SE 1.4.2 = Mantis
ForkJoinPool.commonPool-worker-2: J2SE 1.3 = Kestrel
main: Java SE 6 = Mustang
ForkJoinPool.commonPool-worker-5: J2SE 1.4.1 = Hopper
ForkJoinPool.commonPool-worker-4: J2SE 5.0 = Tiger
ForkJoinPool.commonPool-worker-3: J2SE 1.2.1 = null

(○・ω・)ノ————- forEach (BiFunction, Consumer) ————-
ForkJoinPool.commonPool-worker-5: J2SE 1.3.1 = Ladybird
main: J2SE 1.2 = Playground
ForkJoinPool.commonPool-worker-1: JDK 1.1.4 = Sparkler

(○・ω・)ノ————- forEachEntry (Function Consumer)————-
ForkJoinPool.commonPool-worker-1: J2SE 1.3.1 = Ladybird
ForkJoinPool.commonPool-worker-6: JDK 1.1.4 = Sparkler
ForkJoinPool.commonPool-worker-7: J2SE 1.2 = Playground

(○・ω・)ノ————- search ————-
searchResult: J2SE 1.3.1 = Ladybird

(○・ω・)ノ————- searchKeys ————-
searchKeysResult: J2SE 1.2.2

(○・ω・)ノ————- searchValues ————-
searchValuesResult: Sparkler

(○・ω・)ノ————- searchEntries ————-
searchEntriesResult: JDK 1.1.4 = Sparkler

(○・ω・)ノ————- reduce ————-
reduceResult
JDK 1.1.7 = Brutus
Java SE 9 =
JDK 1.1.8 = Chelsea
Java SE 8 = null
JDK 1.1.5 = Pumpkin
JDK 1.1.6 = Abigail
J2SE 1.2 = Playground
JDK 1.1.4 = Sparkler
Java SE 7 = Dolphin
Java SE 6 = Mustang
J2SE 1.2.2 = Cricket
J2SE 1.3.1 = Ladybird
J2SE 1.2.1 = null
J2SE 1.3 = Kestrel
J2SE 1.4 = Merlin
J2SE 5.0 = Tiger
J2SE 1.4.2 = Mantis
J2SE 1.4.1 = Hopper

(○・ω・)ノ————- reduceKeys ————-
reduceKeysResult
J2SE 1.2.2
J2SE 1.3.1
J2SE 1.2.1
J2SE 1.4.2
J2SE 1.4.1

(○・ω・)ノ————- reduceValues ————-
reduceValuesResult
Brutus
Merlin
Tiger
Mantis
Hopper

(○・ω・)ノ————- reduceEntries ————-
reduceEntriesResult
J2SE 1.2 = Playground
JDK 1.1.4 = Sparkler
J2SE 1.3.1 = Ladybird

(○・ω・)ノ————- reduceMaxValuesResult ————-
10

(○・ω・)ノ————- お終い! ————-

 

それでは、プログラムをざっくり追ってみましょう。

16 行目でこのプログラムを実行しているコンピュータの論理 CPU 数を取得しています。

そして、VM オプションを -Djava.util.concurrent.ForkJoinPool.common.parallelism=7 としてあるので正しく設定されているか確認を 18 行目のコードで行っています。

// CPU
System.out.println(“CPU: ” + ManagementFactory.getOperatingSystemMXBean().getAvailableProcessors());
// -Djava.util.concurrent.ForkJoinPool.common.parallelism=7
System.out.println(“ForkJoinPoolParallelism: ” + ForkJoinPool.getCommonPoolParallelism() + System.getProperty(“line.separator”));

1

20 行目から 38 行目にかけて、ConcurrentHashMap を生成してデータを挿入しています。

ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
map.put(“JDK 1.1.4”, “Sparkler”);
.
.
map.put(“Java SE 9”, “”);

40 行目でマッピング数を取得しています。

System.out.println(“マッピング数: ” + map.mappingCount()
        + System.getProperty(“line.separator”));

public long mappingCount() は、API ドキュメントによると次のように書かれています。

マッピングの数を返します。ConcurrentHashMapにはintで表すことができる数より多くのマッピングが含まれている可能性があるため、

size()のかわりにこのメソッドを使用するようにしてください。返される値は推定値であり、挿入や削除が同時に行われた場合、実際のカウントは異なる可能性があります。

大きな要素を持つ場合は size() の代わりに mappingCount() を使うことができるようになったようですね。

forEach

43 行目から 57 行目までは forEach を試しています。

forEach は次のように 9 種類用意されています。

引数に Function, BiFunction のような関数を含むものもあり便利に使えます。

public void forEach(BiConsumer<? super K,? super V> action)

public void forEach(long parallelismThreshold,
                    BiConsumer<? super K,? super V> action)

public <U> void forEach(long parallelismThreshold,
                        BiFunction<? super K,? super V,? extends U> transformer,
                        Consumer<? super U> action)

public void forEachKey(long parallelismThreshold,
                       Consumer<? super K> action)

public <U> void forEachKey(long parallelismThreshold,
                           Function<? super K,? extends U> transformer,
                           Consumer<? super U> action)

public void forEachValue(long parallelismThreshold,
                         Consumer<? super V> action)

public <U> void forEachValue(long parallelismThreshold,
                             Function<? super V,? extends U> transformer,
                             Consumer<? super U> action)

public void forEachEntry(long parallelismThreshold,
                         Consumer<? super Map.Entry<K,V>> action)

public <U> void forEachEntry(long parallelismThreshold,
                             Function<Map.Entry<K,V>,? extends U> transformer,
                             Consumer<? super U> action)

今回のプログラムでは、

public void forEach(long parallelismThreshold,
                    BiConsumer<? super K,? super V> action)

public <U> void forEach(long parallelismThreshold,
                        BiFunction<? super K,? super V,? extends U> transformer,
                        Consumer<? super U> action)

public <U> void forEachEntry(long parallelismThreshold,
                             Function<Map.Entry<K,V>,? extends U> transformer,
                             Consumer<? super U> action)

を使っています。

第一引数の parallelismThreshold は、このオペレーションを並列的に実行するために必要な(推定の)要素数となっています。いわゆる閾値です。

並行処理をするために第一引数の parallelismThreshold を 1 と設定しています。

それで本当に並行処理が行われているか確認のためにスレッド名を表示させています。

ちょっと注意がいるのは、public void forEach(BiConsumer<? super K,? super V> action) ですね。

これ、引数に parallelismThreshold がありません。

つまり、並行処理されません。

残りの引数は Java SE 8 ではお馴染みの関数ですから特に説明することはないでしょう。

一つだけ API ドキュメントの内容を紹介しておきます。

public <U> void forEach(long parallelismThreshold,
                        BiFunction<? super K,? super V,? extends U> transformer,
                        Consumer<? super U> action)

各(キー, 値)のnullでない各変換に対し、指定されたアクションを実行します。

型パラメータ:
U – トランスフォーマの戻り値の型

パラメータ:
parallelismThreshold – このオペレーションを並列的に実行するために必要な(推定の)要素数
transformer – 要素の変換を返す関数。変換がない場合はnull(その場合、アクションは適用されない)
action – アクション

個人的にはこれが一番使い勝手が良いかなって思っています。

それでは三つの forEach の実行結果を確認します。

はじめに第二引数に BiConsumer を持つ forEach の処理です。

map.forEach(1,
        (key, value) -> System.out.println(Thread.currentThread().getName() + “: ” + key + ” = ” + value));

戻り値無しの処理でスレッド名と key, value を一つの文字列とします。(解りやすいように連結用の文字を追加しています。)

(○・ω・)ノ————- forEach (BiConsumer) ————-
main: JDK 1.1.7 = Brutus
main: Java SE 9 =
main: JDK 1.1.8 = Chelsea
main: Java SE 8 = null
main: JDK 1.1.5 = Pumpkin
main: JDK 1.1.6 = Abigail
main: J2SE 1.2 = Playground
main: JDK 1.1.4 = Sparkler
ForkJoinPool.commonPool-worker-3: J2SE 1.2.2 = Cricket
main: Java SE 7 = Dolphin
ForkJoinPool.commonPool-worker-3: J2SE 1.3.1 = Ladybird
ForkJoinPool.commonPool-worker-4: J2SE 1.4 = Merlin
ForkJoinPool.commonPool-worker-1: J2SE 1.4.2 = Mantis
ForkJoinPool.commonPool-worker-2: J2SE 1.3 = Kestrel
main: Java SE 6 = Mustang
ForkJoinPool.commonPool-worker-5: J2SE 1.4.1 = Hopper
ForkJoinPool.commonPool-worker-4: J2SE 5.0 = Tiger
ForkJoinPool.commonPool-worker-3: J2SE 1.2.1 = null

 

次は、引数が三つの forEach で第二引数に BiFunction を第三引数に Consumer を持っています。

map.forEach(1,
        (key, value) -> key.length() > 7 && value.length() > 7 ? Thread.currentThread().getName() + “: ” + key + ” = ” + value : null,
        System.out::println);

第二引数の BiFunction で key が 7 文字より長くかつ value が 7 文字より長い値をフィルタリングし、スレッド名と key, value を一つの文字列として返しています。(解りやすいように連結用の文字を追加しています。)

第三引数の Consumer ではそれらを標準出力に表示させています。

(○・ω・)ノ————- forEach (BiFunction, Consumer) ————-
ForkJoinPool.commonPool-worker-5: J2SE 1.3.1 = Ladybird
main: J2SE 1.2 = Playground
ForkJoinPool.commonPool-worker-1: JDK 1.1.4 = Sparkler

 

次の forEach は、forEachEntry で Map.Entry に操作を加えます。

map.forEachEntry(1,
        entry -> entry.getKey().length() > 7 && entry.getValue().length() > 7 ? Thread.currentThread().getName() + “: ” + entry.getKey() + ” = ” + entry.getValue() : null,
        System.out::println);

第二引数の Function で、Entry の key と value をそれぞれ取得、そして key が 7 文字より長くかつ value が 7 文字より長い値をフィルタリングし、スレッド名と key, value を一つの文字列として返しています。(解りやすいように連結用の文字を追加しています。)

第三引数の Consumer でではそれらを標準出力に表示させています。

(○・ω・)ノ————- forEachEntry (Function Consumer)————-
ForkJoinPool.commonPool-worker-1: J2SE 1.3.1 = Ladybird
ForkJoinPool.commonPool-worker-6: JDK 1.1.4 = Sparkler
ForkJoinPool.commonPool-worker-7: J2SE 1.2 = Playground

forEach だけでも至れり尽くせりで便利に使えますね。(^_^)

Function, BiFunction が使えるのって幸せです!

search

それでは search を見ていきましょう。

59 行目から 78 行目まで  search を試しています。

search は 4 種類の優れたメソッドが追加されました。

それぞれ API ドキュメントの内容を見ていきましょう。

public <U> U search(long parallelismThreshold,
                    BiFunction<? super K,? super V,? extends U> searchFunction)

指定された検索関数を各(キー、値)に適用し、nullでない結果を返します(存在しない場合はnull)。
成功した場合、その後の要素の処理は抑制され、検索関数の他の並列呼出しの結果は無視されます。

型パラメータ:
U – 検索関数の戻り値の型

パラメータ:
parallelismThreshold – このオペレーションを並列的に実行するために必要な(推定の)要素数
searchFunction – 成功した場合はnull以外の結果を返し、それ以外の場合はnullを返す関数。

戻り値:
各(キー、値)に対して指定された検索関数を適用した場合のnull以外の結果。存在しない場合はnull

 

public <U> U searchKeys(long parallelismThreshold,
                        Function<? super K,? extends U> searchFunction)

各キーに指定された検索関数を適用したnull以外の結果を返します。結果がない場合はnullを返します。
成功した場合、その後の要素の処理は抑制され、検索関数の他の並列呼出しの結果は無視されます。

型パラメータ:
U – 検索関数の戻り値の型

パラメータ:
parallelismThreshold – このオペレーションを並列的に実行するために必要な(推定の)要素数
searchFunction – 成功した場合はnull以外の結果を返し、それ以外の場合はnullを返す関数。

戻り値:
各キーに対して指定された検索関数を適用した場合のnull以外の結果。存在しない場合はnull

 

public <U> U searchValues(long parallelismThreshold,
                          Function<? super V,? extends U> searchFunction)

各値に指定された検索関数を適用したnull以外の結果を返します。結果がない場合はnullを返します。
成功した場合、その後の要素の処理は抑制され、検索関数の他の並列呼出しの結果は無視されます。

型パラメータ:
U – 検索関数の戻り値の型

パラメータ:
parallelismThreshold – このオペレーションを並列的に実行するために必要な(推定の)要素数
searchFunction – 成功した場合はnull以外の結果を返し、それ以外の場合はnullを返す関数。

戻り値:
各値に指定された検索関数を適用したnull以外の結果。存在しない場合はnull

 

public <U> U searchEntries(long parallelismThreshold,
                           Function<Map.Entry<K,V>,? extends U> searchFunction)

各エントリに指定された検索関数を適用したnull以外の結果を返します。結果がない場合はnullを返します。
成功した場合、その後の要素の処理は抑制され、検索関数の他の並列呼出しの結果は無視されます。

型パラメータ:
U – 検索関数の戻り値の型

パラメータ:
parallelismThreshold – このオペレーションを並列的に実行するために必要な(推定の)要素数
searchFunction – 成功した場合はnull以外の結果を返し、それ以外の場合はnullを返す関数。

戻り値:
各エントリに指定された検索関数を適用したnull以外の結果。存在しない場合はnull

API ドキュメントを見るとそれぞれ key と value に対する操作、key に対する操作、value に対する操作、Map.Entry に対する操作を行うように分けられているようです。

search でも Function, BiFunction が便利に検索関数として使われています。

それでは順番にプログラムの処理を見ていきましょう。

key と value による検索

        String searchResult = map.search(1, (key, value) -> key.length() > 7 && value.length() > 7 ? key + ” = ” + value : null);

key が 7 文字より長くかつ value が 7 文字より長い値をフィルタリングし、key, value を一つの文字列として返しています。(解りやすいように連結用の文字を追加しています。)

プログラムの実行結果は次のようになりました。

(○・ω・)ノ————- search ————-
searchResult: J2SE 1.3.1 = Ladybird

API ドキュメントによれば、複数検索条件に該当する場合、最初に検索に成功した一つだけが選択され、他は破棄されます。

よって検索条件に該当するものが複数ある場合必ず結果が同じになるとは限らない。

key による検索

String searchKeysResult = map.searchKeys(1, key -> key.length() > 7 ? key : null);

key が 7 文字より長い値をフィルタリングし、key を返しています。

プログラムの実行結果は次のようになりました。

(○・ω・)ノ————- searchKeys ————-
searchKeysResult: J2SE 1.2.2

value による検索

String searchValuesResult = map.searchValues(1, value -> value.length() > 7 ? value : null);

value が 7 文字より長い値をフィルタリングし、value を返しています。

プログラムの実行結果は次のようになりました。

(○・ω・)ノ————- searchValues ————-
searchValuesResult: Sparkler

Map.Entry による検索

String searchEntriesResult = map.searchEntries(1,
        entry -> entry.getKey().length() > 7 && entry.getValue().length() > 7 ? entry.getKey() + ” = ” + entry.getValue() : null);

Entry の key と value をそれぞれ取得、そして key が 7 文字より長くかつ value が 7 文字より長い値をフィルタリングし、key, value を一つの文字列として返しています。(解りやすいように連結用の文字を追加しています。)

プログラムの実行結果は次のようになりました。

(○・ω・)ノ————- searchEntries ————-
searchEntriesResult: JDK 1.1.4 = Sparkler

reduce

それでは reduce を見てみましょう。

80 行目から 114 行目まで reduce を試しています。

reduce も search 同様に key と value, key, value, Map.Entry それぞれに操作を行うメソッドを用意しています。

API ドキュメントを見てみましょう。

public <U> U reduce(long parallelismThreshold,
                    BiFunction<? super K,? super V,? extends U> transformer,
                    BiFunction<? super U,? super U,? extends U> reducer)

指定されたリデューサを使用して値を結合することにより、すべての(キー、値)ペアの指定された変換の累積結果を返します。結果がない場合はnullを返します。

型パラメータ:
U – トランスフォーマの戻り値の型

パラメータ:
parallelismThreshold – このオペレーションを並列的に実行するために必要な(推定の)要素数
transformer – 要素の変換を返す関数。変換がない場合はnull(その場合、値は結合されない)
reducer – 交換可能性と結合性を持つ結合関数

戻り値:
すべての(キー、値)ペアの指定された変換を累積した結果

public <U> U reduceKeys(long parallelismThreshold,
                        Function<? super K,? extends U> transformer,
                        BiFunction<? super U,? super U,? extends U> reducer)

指定されたリデューサを使用して値を結合することにより、すべてのキーの指定された変換の累積結果を返します。結果がない場合はnullを返します。

型パラメータ:
U – トランスフォーマの戻り値の型

パラメータ:
parallelismThreshold – このオペレーションを並列的に実行するために必要な(推定の)要素数
transformer – 要素の変換を返す関数。変換がない場合はnull(その場合、値は結合されない)
reducer – 交換可能性と結合性を持つ結合関数

戻り値:
すべてのキーの指定された変換を累積した結果

public <U> U reduceValues(long parallelismThreshold,
                          Function<? super V,? extends U> transformer,
                          BiFunction<? super U,? super U,? extends U> reducer)

指定されたリデューサを使用して値を結合することにより、すべての値の指定された変換の累積結果を返します。結果がない場合はnullを返します。

型パラメータ:
U – トランスフォーマの戻り値の型

パラメータ:
parallelismThreshold – このオペレーションを並列的に実行するために必要な(推定の)要素数
transformer – 要素の変換を返す関数。変換がない場合はnull(その場合、値は結合されない)
reducer – 交換可能性と結合性を持つ結合関数

戻り値:
すべての値の指定された変換を累積した結果

public <U> U reduceEntries(long parallelismThreshold,
                           Function<Map.Entry<K,V>,? extends U> transformer,
                           BiFunction<? super U,? super U,? extends U> reducer)

指定されたリデューサを使用して値を結合することにより、すべてのエントリの指定された変換の累積結果を返します。結果がない場合はnullを返します。

型パラメータ:
U – トランスフォーマの戻り値の型

パラメータ:
parallelismThreshold – このオペレーションを並列的に実行するために必要な(推定の)要素数
transformer – 要素の変換を返す関数。変換がない場合はnull(その場合、値は結合されない)
reducer – 交換可能性と結合性を持つ結合関数

戻り値:
すべてのエントリの指定された変換を累積した結果

reduce は、int, long, double などを便利に使えるようにする reduce○△×□ToInt, reduce○△×□ToLong, reduce○△×□ToDouble なども用意されています。

これらは、入力をプリミティブ型に変換して基準値を指定して累積計算して値を返すというものです。

今回のプログラムでは、reduceValuesToInt だけ試しています。

API ドキュメントでは次のようになっています。

public int reduceValuesToInt(long parallelismThreshold,
                             ToIntFunction<? super V> transformer,
                             int basis,
                             IntBinaryOperator reducer)

指定されたリデューサを使用して値を結合し、指定された基準を識別値として使用して、すべての値の指定された変換の累積結果を返します。

パラメータ:
parallelismThreshold – このオペレーションを並列的に実行するために必要な(推定の)要素数
transformer – 要素の変換を返す関数basis – リダクションの識別(初期のデフォルト値)
reducer – 交換可能性と結合性を持つ結合関数

戻り値:すべての値の指定された変換を累積した結果

それではプログラムを見ていきましょう。

key と value によるリデュース

String reduceResult = map.reduce(1,
        (key, value) -> key + ” = ” + value,
        (s1, s2) -> s1 + System.getProperty(“line.separator”) + s2);

第二引数の BiFunction で要素の変換処理(key, value を一つの文字列として返しています。(解りやすいように連結用の文字を追加しています。)をして返し、

第三引数の BiFunction で返された値をシステムの改行文字を加えて結合して返します。

このプログラムの実行結果は次のようになります。

(○・ω・)ノ————- reduce ————-
reduceResult
JDK 1.1.7 = Brutus
Java SE 9 =
JDK 1.1.8 = Chelsea
Java SE 8 = null
JDK 1.1.5 = Pumpkin
JDK 1.1.6 = Abigail
J2SE 1.2 = Playground
JDK 1.1.4 = Sparkler
Java SE 7 = Dolphin
Java SE 6 = Mustang
J2SE 1.2.2 = Cricket
J2SE 1.3.1 = Ladybird
J2SE 1.2.1 = null
J2SE 1.3 = Kestrel
J2SE 1.4 = Merlin
J2SE 5.0 = Tiger
J2SE 1.4.2 = Mantis
J2SE 1.4.1 = Hopper

key によるリデュース

String reduceKeysResult = map.reduceKeys(1,
        key -> key.length() > 9 ? key : null,
        (s1, s2) -> s1 + System.getProperty(“line.separator”) + s2);

第二引数の Function で要素の変換処理 key が 9 文字より長い値をフィルタリングし、key を返し、

第三引数の BiFunction で返された値をシステムの改行文字を加えて結合して返します。

このプログラムの実行結果は次のようになります。

(○・ω・)ノ————- reduceKeys ————-
reduceKeysResult
J2SE 1.2.2
J2SE 1.3.1
J2SE 1.2.1
J2SE 1.4.2
J2SE 1.4.1

value によるリデュース

String reduceValuesResult = map.reduceValues(1,
        value -> value.length() > 4 && value.length() < 7 ? value : null,
        (s1, s2) -> s1 + System.getProperty(“line.separator”) + s2);

第二引数の Function で要素の変換処理 value が 4 文字より長く、かつ value が 7 文字より短い値をフィルタリングし、value を返し、

第三引数の BiFunction で返された値をシステムの改行文字を加えて結合して返します。

このプログラムの実行結果は次のようになります。

(○・ω・)ノ————- reduceValues ————-
reduceValuesResult
Brutus
Merlin
Tiger
Mantis
Hopper

Map.Entry によるリデュース

String reduceEntriesResult = map.reduceEntries(1,
        entry -> entry.getKey().length() > 7 && entry.getValue().length() > 7 ? entry.getKey() + ” = ” + entry.getValue() : null,
        (s1, s2) -> s1 + System.getProperty(“line.separator”) + s2);

第二引数の Function で要素の変換処理 Map.Entry の key と value をそれぞれ取得、そして key が 7 文字より長くかつ value が 7 文字より長い値をフィルタリングし、

Map.Entry の key, value を一つの文字列として返しています。(解りやすいように連結用の文字を追加しています。)

第三引数の BiFunction で返された値をシステムの改行文字を加えて結合して返します。

このプログラムの実行結果は次のようになります。

(○・ω・)ノ————- reduceEntries ————-
reduceEntriesResult
J2SE 1.2 = Playground
JDK 1.1.4 = Sparkler
J2SE 1.3.1 = Ladybird

reduceValuesToInt

int reduceMaxValuesResult = map.reduceValuesToInt(1,
        String::length,
        0,
        Integer::max);

第二引数の ToIntFunction で value の文字列の長さを返し(int 値)、

第三引数の基準値を元に、

第四引数の IntBinaryOperator で最大値を求める累積処理を行い最大値を返します。

このプログラムの実行結果は次のようになります。

(○・ω・)ノ————- reduceMaxValuesResult ————-
10

これで、ざっくり試してみたのでお終いです。

(○・ω・)ノ————- お終い! ————-

今回は試さなかったけど次のような優れものが Java SE 8 から追加されてます。

public static <K> ConcurrentHashMap.KeySetView<K,Boolean> newKeySet()

指定された型からBoolean.TRUEへの、ConcurrentHashMapに連動する新しいSetを作成します。

型パラメータ:
K – 返されるセットの要素型

戻り値:
新しいセット

public static <K> ConcurrentHashMap.KeySetView<K,Boolean> newKeySet(int initialCapacity)

指定された型からBoolean.TRUEへの、ConcurrentHashMapに連動する新しいSetを作成します。

型パラメータ:
K – 返されるセットの要素型

パラメータ:
initialCapacity – 実装は、この多数の要素を格納するように内部のサイズ設定を実行する。

戻り値:
新しいセット

例外:
IllegalArgumentException – 要素の初期容量が負の場合

Java SE 8 から Map が強力になった。

特に ConcurrentHashMap は、J2SE5.0 の時と比べるとかなり使い勝手がよくなった。(^_^)

さて、ここでもう一度 ConcurrentHashMap の特徴を思い出してみましょう。

ConcurrentHashMap は、すべてのメソッドを共通のロックで同期化してアクセスを一度に一つのスレッドに限定する代わりに、

ロックストライピングという小さな粒度のロック方式を採用することにより並行処理能力を高めています。

ConcurrentHashMap は、Iterator および Enumeration は、ある時点または反復子/列挙の作成以降のハッシュテーブルの状態を反映する要素を返すように設計されているため、

ConcurrentModificationException をスローすることはありません。

なにより興味深いのはデータの取得時にはブロックすることがなく、更新についてはユーザーが並行レベルを設定できるということです。

ちょっと最後の並行レベルを設定できると言うところを試してみました。

ConcurrentHashMap インスタンスを生成するコンストラクタは 5 種類あります。

その中で並行レベルを設定できるのは次の引数を持つコンストラクタです。

public ConcurrentHashMap(int initialCapacity,
                         float loadFactor,
                         int concurrencyLevel)

指定された要素数(initialCapacity)、テーブル密度(loadFactor)および並行更新数のしきい値(concurrencyLevel)に基づく初期テーブル・サイズで、新しい空のマップを作成します。

パラメータ:
initialCapacity – 初期容量。負荷係数を指定すると、実装はこの数の要素を格納できるように内部のサイズ設定を行う。
loadFactor – 初期テーブル・サイズを設定するための負荷係数(テーブル密度)
concurrencyLevel – 並行して更新しているスレッドの推定数。実装はこの値をサイズ設定のヒントとして使用できる。

例外:
IllegalArgumentException – 初期容量が負であるか、負荷係数またはconcurrencyLevelが正でない場合

第三引数の int concurrencyLevel が並行レベルにあたります。

Java SE 8 の API ドキュメントでは初期容量、負荷係数、並行レベルのデフォルト値が明記されなくなりました。(初期容量は 16 とありました)

ソースを覗いてみると初期容量 16, 負荷係数 0.75f, 並行レベル 16 となっています。昔と変わって無さそうです。

並行レベルの設定で大きな違いがでるのかつぎのような ConcurrentHashMap インスタンスを生成して試してみました。

ConcurrentHashMap<Long, Long> map = new ConcurrentHashMap<>();
ConcurrentHashMap<Long, Long> map2 = new ConcurrentHashMap<>(16, 0.75f, 1);

下記 udDate() メソッドを同時に実行するスレッドを 32 個作って試してみました。

CompletableFuture<Void> cnt_1 = CompletableFuture.runAsync(() -> upDate.upDate()); こんなのを 32 個 (^_^;)

void upDate() {
    for (long i = 0; i < 100_000_000; i++) {
        map.computeIfAbsent(i, (k) -> k);
        if ((i & 0b1) == 0) {
            map.remove(i);
        }
    }
    for (long i = 0; i < 100_000_000; i++) {
        map.computeIfAbsent(i, (k) -> k * 10);
        map.getOrDefault(i, – 1L);
    }
}

プログラムの実行結果は次のようになりました。

ConcurrentHashMap<Long, Long> map = new ConcurrentHashMap<>(); // 5分33秒14428361700004189

ConcurrentHashMap<Long, Long> map2 = new ConcurrentHashMap<>(16, 0.75f, 1); // 34分42秒4334219810002651

凄く大差がつきました。

ちなみに実行環境は、先ほどのプログラムを実行しているコンピュータで論理 CPU 数 32 個、VM オプションの ForkJoinPool の制限無しで試してみました。

以上、ConcurrentHashMap でのお遊びはこれでお終いです。

Hatena タグ:

java.util.ConcurrentModificationException 対処法

Java

今日のエントリーも J2SE5.0 時代の古いものです。

しかし、あまり知られていない(目にする機会が少ない)と思われるのでブログ更新用のネタとしました。

今回の問題は二つのスレッドが並行して List オブジェクトにアクセスするよくある問題です。

ここで思い出してください。

Java の Iterator は Fail-Fast に設計されています。

これはイテレーション処理開始後にコレクションの内容が変更されたことを検出したら直ち java.util.ConcurrentModificationException をスローします。

これを回避するには一番手っ取り早いのは synchronized ブロックを使って制御するのが楽です。

synchronizedList() を使ってるから大丈夫だと思っている人は泣いてください。(ヲヒ

Iterator なめたらあかんぜよ!

さて、いったい何を言いたいのか良く解らないと思うので簡潔にまとめていないダラダラとしたサンプルコードをご覧ください。

このプログラムの実行結果は次のような実行時エラーがでます。

堀北真希 Girls_1
北川景子 Girls_1
堀北真希 Girls_2
Exception in thread “main” java.util.concurrent.CompletionException: java.util.ConcurrentModificationException
    at java.util.concurrent.CompletableFuture.encodeThrowable(CompletableFuture.java:273)
    at java.util.concurrent.CompletableFuture.completeThrowable(CompletableFuture.java:280)
    at java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1584)
    at java.util.concurrent.CompletableFuture$AsyncSupply.exec(CompletableFuture.java:1574)
    at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:289)
    at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1056)
    at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1689)
    at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:157)
Caused by: java.util.ConcurrentModificationException
    at java.util.ArrayList.forEach(ArrayList.java:1252)
    at jp.yucchi.girlfriends_cme.GirlFriends_1.updateGirlFriend(GirlFriends_1.java:27)
    at jp.yucchi.girlfriends_cme.GirlFriends_CME.lambda$main$0(GirlFriends_CME.java:26)
    at jp.yucchi.girlfriends_cme.GirlFriends_CME$$Lambda$1/424058530.get(Unknown Source)
    at java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1582)
    … 5 more
Java Result: 1

このプログラムは、GirlFriends_1 スレッドで要素を追加し、イテレーション処理を要素を一つ出力させるごとに一秒間スレッドをスリープさせています。

GirlFriends_2 スレッドでは、要素の削除処理の開始を一秒遅らせています。

GirlFriends_1 スレッドのイテレーション処理中に  GirlFriends_2 スレッド によって List の内容が変更されてしまいます。

二つのスレッドで一つの List に何も考えずに並行処理している無残な結果が悲しいですね。

では、安全に並行処理するために synchronized ブロック を使って終わりではつまらないのでもう一つの解決方法を紹介します。

これを使う機会って滅多にないでしょうけど java.util.concurrent.CopyOnWriteArrayList<E> ってのがあります。

API ドキュメントには次のように書かれています。

基になる配列の新しいコピーを作成することにより、すべての推移的操作(add、setなど)が実装されるArrayListのスレッド・セーフな変数です。
通常、これは非常に効率が悪いのですが、トラバーサル操作が変更を数の点で大幅に上回る場合には、代替手段よりも効率が良い場合があります。
また、これは、トラバーサルを同期できない場合や、同期することを望まないが、並行スレッド間の干渉を排除する必要がある場合に有用です。
「スナップショット」スタイルのイテレータ・メソッドは、イテレータの作成時点での配列状態への参照を使用します。
この配列がイテレータの有効期間中に変更されることは決してないため、干渉は不可能であり、イテレータはConcurrentModificationExceptionをスローしないことが保証されます。
イテレータは、イテレータの作成以降のリストへの追加、削除、または変更を反映しません。
イテレータ自体に対する要素変更操作(remove、setおよびadd)はサポートされません。
これらのメソッドは、UnsupportedOperationExceptionをスローします。
nullを含むすべての要素が許可されます。
メモリー整合性効果: ほかの並行処理コレクションと同様、オブジェクトをCopyOnWriteArrayListに配置する前のスレッド内のアクションは、
別のスレッドでのその要素へのアクセスまたはCopyOnWriteArrayListからの削除に続くアクションよりも前に発生します。

今回のケースではお手軽に使えそうですね。

そう言うことでまたダラダラとコードをご覧ください。と、思ったけど List を CopyOnWriteArrayList に変更するだけなのでやめておきます。

いちおう、 GitHub にアップしておきますので見たい方はどうぞ。

https://github.com/Yucchi-1995/GirlFriends

プログラムの実行結果は次のようになります。

堀北真希 Girls_1
北川景子 Girls_1
堀北真希 Girls_2
桐谷美玲 Girls_1
新垣結衣 Girls_1
剛力彩芽 Girls_1
GirlFriend_1 List
堀北真希
綾瀬はるか
GirlFriend_2 List
堀北真希
綾瀬はるか
GirlFriend List
堀北真希
綾瀬はるか
新垣結衣
剛力彩芽

java.util.ConcurrentModificationException が投げられることなく並行処理が完了しています。

とても簡単です。

API ドキュメントにも書いてありますが CopyOnWriteArrayList はイテレータの作成時点での配列状態への参照を使用し、

この配列がイテレータの有効期間中に変更されることは決してないと言い切っているとおり、

プログラムの実行結果より Girls_1 スレッド、Girls_2 スレッドのイテレーション処理出力がそのとおりに出力されているのが確認できます。

堀北真希 Girls_1
北川景子 Girls_1
堀北真希 Girls_2
桐谷美玲 Girls_1
新垣結衣 Girls_1
剛力彩芽 Girls_1

そして、Girls_1 スレッド、Girls_2 スレッドから返される List の要素は「堀北真希」の一つとなります。

GirlFriend_1 List、GirlFriend_2 List の出力が

GirlFriend_1 List
堀北真希
綾瀬はるか
GirlFriend_2 List
堀北真希
綾瀬はるか

となっているのは main スレッドに戻ってから「綾瀬はるか」が追加され出力されているからです。

そして、最後に「新垣結衣」と「剛力彩芽」を追加して List を表示させています。

CopyOnWriteArrayList って本当に優れものですね!

どうでもいいことですが、このプログラムの欠陥は「北川景子」を削除して、あとで再び List に追加する予定だったのをすっかり忘れてしまったことです。(^_^;)

Hatena タグ:

食事する哲学者の問題(Dining Philosophers Problem) Semaphore で遊ぶ その2

Java NetBeans

食事する哲学者の問題(Dining Philosophers Problem)を J2SE5.0 で追加された java.util.concurrent.Semaphore を使ってプログラムを組んでみます。

Semaphore については前回のエントリーでざっくり説明し、シンプルなプログラムも紹介しています。

Semaphore で遊ぶ

それでは、食事する哲学者の問題(Dining Philosophers Problem)とは何か調べてみます。

WikiPedia によると、食事する哲学者の問題(Dining Philosophers Problem)とは、並列処理に関する問題を一般化した例である。古典的なマルチプロセスの同期(排他制御)問題である。とされています。

もともとは、1965年、エドガー・ダイクストラは5台のコンピュータが5台のテープ装置に競合アクセスするという同期問題を提示した。

間もなく、この問題はアントニー・ホーアによって「食事する哲学者の問題」に変形して語られることとなった。

どうして食事する哲学者にすり替わったかは置いといて・・・

【問題】

5人の哲学者が食事したり、考え事をしたりしている。

かれらの前には、真ん中にスパゲッティの入った大きなボールが置かれた丸い食卓がある。

その食卓には5枚の皿が置かれ、皿と皿の間にフォークが1本ずつ置かれている。

スパゲッティをボールから取って自分の皿によそうには2本のフォークを必要とし、哲学者は一度に1本のフォークしかテーブルから取ることができない。

(左右の手で同時に両側にある2本のフォークを取ることはできない、という意味。まずどちらかの側のフォークを取ってから、逆の側のフォークを取る。)

哲学者同士は決して会話をしない。

このため、5人が左のフォークを手にとって右のフォークが食卓に置かれるのを待つという危険なデッドロック状態が発生する可能性がある。

スパゲッティではないけどこういう状況ですね。

cake1

【解説】

本来、デッドロック問題の解説手段として使われた。このシステムがデッドロックに到るのは「満たされない要求の円環」が存在する場合である。

例えば、哲学者 P1 が哲学者 P2 の手にしているフォークを待っていて、P2 は哲学者 P3 のものを……といったように円環形に要求が連鎖する。

例えば、それぞれの哲学者が次のように振る舞うとする。

1.左のフォークが得られるまで思索して待ち、フォークを取り上げる。

2.右のフォークが得られるまで思索して待ち、フォークを取り上げる。

3.食事する。

4.右のフォークを置く。

5.左のフォークを置く。

6.最初にもどって繰り返す。

この解法は正しくない。

このやりかたではデッドロックが発生し、全哲学者が左のフォークを持ち、右のフォークが持てるようになるのを待つという状況が発生しうる。

その場合、右のフォークは決して得られない (>_<。)

今回はこの方法でデッドロックを起こさないように Semaphore を使ってリソースへのアクセスを制限し対処するプログラムを組んでみることにします。

これって少し考えてみれば食事をしている両隣の哲学者はフォークをとれない状況ができるので同時に食事出来る哲学者の最大数は5人の場合は二人となります。

つまり、使われるフォークの数は4本となります。

これを利用すれば比較的簡単に Semaphore を利用してプログラムを組むことができますね。

それではプログラムを組んでみることにしますが、素直に哲学者に登場していただくより綺麗な女性を食事にお誘いしました。

今回デッドロックを避けるために使う Semaphore はウェイターの役割をはたします。

つまり、スパゲッティを食べる権利( パーミット取得 )があることを許可してくれます。

綺麗な女性は左のフォーク、そして右のフォークと使うことができればスパゲッティを食べることができます。

ただし、隣の女性がフォークを使っていたら( ロックがかかっていたら )フォークがあくのを待たなければ( ロックが解放される )いけません。

ウエイターは綺麗な女性がスパゲッティを食べ終わり左右のフォークを置いたなら、

その綺麗な女性のスパゲッティを食べる権利( パーミット )を戻してもらい、

次に待機している綺麗な女性がいれば「どうぞ、お食べください」とスパゲッティを食べる権利( パーミット )を与えます。。

そして綺麗な女性はフォークが空いていれば手に取りスパゲッティを食べられます。

フォークが空いていなければ待ちます。

これの繰り返しです。

最大二人の綺麗な女性がスパゲッティを同時に食べることがこれで可能となります。

左右のフォークの使用状況( ロックの状態 )によりフォーク待ち時間が発生してしまいますがこれでデッドロックは解消されます。

きっと綺麗な女性達は美味しいスパゲッティをお腹いっぱい食べて満足してお帰りになられたでしょう。

これをプログラムにすると次のようになりました。

プログラムを実行させてデッドロックが発生せず動作するか確認します。

桐谷美玲 左のフォークをとりあげる思索した時間は、0秒7838E-5
北川景子 左のフォークをとりあげる思索した時間は、0秒8168E-5
桐谷美玲 右のフォークをとりあげる思索した時間は、0秒313E-6
桐谷美玲 1回目の食事
桐谷美玲 1回目食事終了
北川景子 右のフォークをとりあげる思索した時間は、1秒3359217400000001
北川景子 1回目の食事
剛力彩芽 左のフォークをとりあげる思索した時間は、0秒9820000000000003E-6
北川景子 1回目食事終了
剛力彩芽 右のフォークをとりあげる思索した時間は、1秒3721449000000001
剛力彩芽 1回目の食事
堀北真希 左のフォークをとりあげる思索した時間は、0秒9820000000000003E-6
堀北真希 右のフォークをとりあげる思索した時間は、0秒9820000000000003E-6
堀北真希 1回目の食事
剛力彩芽 1回目食事終了
堀北真希 1回目食事終了
新垣結衣 左のフォークをとりあげる思索した時間は、0秒375070565
桐谷美玲 左のフォークをとりあげる思索した時間は、0秒9820000000000003E-6
新垣結衣 右のフォークをとりあげる思索した時間は、0秒9820000000000003E-6
桐谷美玲 右のフォークをとりあげる思索した時間は、0秒652E-6
桐谷美玲 2回目の食事
新垣結衣 1回目の食事
新垣結衣 1回目食事終了
北川景子 左のフォークをとりあげる思索した時間は、0秒652E-6
桐谷美玲 2回目食事終了
北川景子 右のフォークをとりあげる思索した時間は、0秒08200188600000001
北川景子 2回目の食事
剛力彩芽 左のフォークをとりあげる思索した時間は、0秒652E-6
北川景子 2回目食事終了
剛力彩芽 右のフォークをとりあげる思索した時間は、0秒583391387
堀北真希 左のフォークをとりあげる思索した時間は、0秒9820000000000003E-6
堀北真希 右のフォークをとりあげる思索した時間は、0秒652E-6
堀北真希 2回目の食事
剛力彩芽 2回目の食事
堀北真希 2回目食事終了
新垣結衣 左のフォークをとりあげる思索した時間は、0秒652E-6
剛力彩芽 2回目食事終了
新垣結衣 右のフォークをとりあげる思索した時間は、0秒436658855
桐谷美玲 左のフォークをとりあげる思索した時間は、0秒652E-6
桐谷美玲 右のフォークをとりあげる思索した時間は、0秒652E-6
新垣結衣 2回目の食事
桐谷美玲 3回目の食事
新垣結衣 2回目食事終了
北川景子 左のフォークをとりあげる思索した時間は、0秒294E-6
桐谷美玲 3回目食事終了
北川景子 右のフォークをとりあげる思索した時間は、0秒23805942200000002
北川景子 3回目の食事
堀北真希 左のフォークをとりあげる思索した時間は、0秒6510000000000002E-6
堀北真希 右のフォークをとりあげる思索した時間は、0秒9820000000000003E-6
堀北真希 3回目の食事
北川景子 3回目食事終了
剛力彩芽 左のフォークをとりあげる思索した時間は、0秒312E-6
剛力彩芽 右のフォークをとりあげる思索した時間は、0秒6510000000000002E-6
剛力彩芽 3回目の食事
堀北真希 3回目食事終了
新垣結衣 左のフォークをとりあげる思索した時間は、0秒652E-6
剛力彩芽 3回目食事終了
新垣結衣 右のフォークをとりあげる思索した時間は、0秒18132323900000002
新垣結衣 3回目の食事
桐谷美玲 左のフォークをとりあげる思索した時間は、0秒652E-6
桐谷美玲 右のフォークをとりあげる思索した時間は、0秒9820000000000003E-6
桐谷美玲 4回目の食事
新垣結衣 3回目食事終了
北川景子 左のフォークをとりあげる思索した時間は、0秒9820000000000003E-6
桐谷美玲 4回目食事終了
北川景子 右のフォークをとりあげる思索した時間は、0秒39374600600000004
北川景子 4回目の食事
堀北真希 左のフォークをとりあげる思索した時間は、0秒652E-6
堀北真希 右のフォークをとりあげる思索した時間は、0秒312E-6
堀北真希 4回目の食事
堀北真希 4回目食事終了
剛力彩芽 左のフォークをとりあげる思索した時間は、0秒9640000000000005E-6
北川景子 4回目食事終了
剛力彩芽 右のフォークをとりあげる思索した時間は、0秒023616073
剛力彩芽 4回目の食事
新垣結衣 左のフォークをとりあげる思索した時間は、0秒6510000000000002E-6
剛力彩芽 4回目食事終了
新垣結衣 右のフォークをとりあげる思索した時間は、0秒7176410350000001
桐谷美玲 左のフォークをとりあげる思索した時間は、0秒312E-6
新垣結衣 4回目の食事
桐谷美玲 右のフォークをとりあげる思索した時間は、0秒9820000000000003E-6
桐谷美玲 5回目の食事
桐谷美玲 5回目食事終了
堀北真希 左のフォークをとりあげる思索した時間は、0秒9820000000000003E-6
新垣結衣 4回目食事終了
堀北真希 右のフォークをとりあげる思索した時間は、0秒718831517
堀北真希 5回目の食事
北川景子 左のフォークをとりあげる思索した時間は、0秒9820000000000003E-6
北川景子 右のフォークをとりあげる思索した時間は、0秒3543E-5
北川景子 5回目の食事
堀北真希 5回目食事終了
剛力彩芽 左のフォークをとりあげる思索した時間は、0秒6430000000000004E-6
北川景子 5回目食事終了
剛力彩芽 右のフォークをとりあげる思索した時間は、0秒140569047
剛力彩芽 5回目の食事
桐谷美玲 左のフォークをとりあげる思索した時間は、0秒981E-6
桐谷美玲 右のフォークをとりあげる思索した時間は、0秒9820000000000003E-6
桐谷美玲 6回目の食事
桐谷美玲 6回目食事終了
新垣結衣 左のフォークをとりあげる思索した時間は、0秒6420000000000003E-6
剛力彩芽 5回目食事終了
新垣結衣 右のフォークをとりあげる思索した時間は、0秒256017732
新垣結衣 5回目の食事
堀北真希 左のフォークをとりあげる思索した時間は、0秒9820000000000003E-6
新垣結衣 5回目食事終了
堀北真希 右のフォークをとりあげる思索した時間は、0秒908995176
堀北真希 6回目の食事
北川景子 左のフォークをとりあげる思索した時間は、0秒973E-6
北川景子 右のフォークをとりあげる思索した時間は、0秒313E-6
北川景子 6回目の食事
堀北真希 6回目食事終了
北川景子 6回目食事終了
桐谷美玲 左のフォークをとりあげる思索した時間は、0秒156232277
剛力彩芽 左のフォークをとりあげる思索した時間は、0秒3210000000000001E-6
桐谷美玲 右のフォークをとりあげる思索した時間は、0秒652E-6
桐谷美玲 7回目の食事
剛力彩芽 右のフォークをとりあげる思索した時間は、0秒9820000000000003E-6
剛力彩芽 6回目の食事
桐谷美玲 7回目食事終了
新垣結衣 左のフォークをとりあげる思索した時間は、0秒312E-6
剛力彩芽 6回目食事終了
新垣結衣 右のフォークをとりあげる思索した時間は、0秒200991297
新垣結衣 6回目の食事
堀北真希 左のフォークをとりあげる思索した時間は、0秒652E-6
新垣結衣 6回目食事終了
堀北真希 右のフォークをとりあげる思索した時間は、1秒3822943870000002
堀北真希 7回目の食事
北川景子 左のフォークをとりあげる思索した時間は、0秒9820000000000003E-6
北川景子 右のフォークをとりあげる思索した時間は、0秒3030000000000004E-6
北川景子 7回目の食事
堀北真希 7回目食事終了
北川景子 7回目食事終了
桐谷美玲 左のフォークをとりあげる思索した時間は、0秒089658099
桐谷美玲 右のフォークをとりあげる思索した時間は、0秒9820000000000003E-6
桐谷美玲 8回目の食事
剛力彩芽 左のフォークをとりあげる思索した時間は、0秒625000000000001E-6
剛力彩芽 右のフォークをとりあげる思索した時間は、0秒313E-6
剛力彩芽 7回目の食事
剛力彩芽 7回目食事終了
新垣結衣 左のフォークをとりあげる思索した時間は、0秒955E-6
新垣結衣 右のフォークをとりあげる思索した時間は、0秒652E-6
新垣結衣 7回目の食事
桐谷美玲 8回目食事終了
堀北真希 左のフォークをとりあげる思索した時間は、0秒3210000000000001E-6
新垣結衣 7回目食事終了
堀北真希 右のフォークをとりあげる思索した時間は、0秒8264462760000001
北川景子 左のフォークをとりあげる思索した時間は、0秒6510000000000002E-6
堀北真希 8回目の食事
北川景子 右のフォークをとりあげる思索した時間は、0秒3210000000000001E-6
北川景子 8回目の食事
堀北真希 8回目食事終了
剛力彩芽 左のフォークをとりあげる思索した時間は、0秒6420000000000003E-6
北川景子 8回目食事終了
剛力彩芽 右のフォークをとりあげる思索した時間は、0秒217371327
剛力彩芽 8回目の食事
桐谷美玲 左のフォークをとりあげる思索した時間は、0秒9820000000000003E-6
桐谷美玲 右のフォークをとりあげる思索した時間は、0秒312E-6
桐谷美玲 9回目の食事
剛力彩芽 8回目食事終了
新垣結衣 左のフォークをとりあげる思索した時間は、0秒9820000000000003E-6
新垣結衣 右のフォークをとりあげる思索した時間は、0秒6510000000000002E-6
新垣結衣 8回目の食事
桐谷美玲 9回目食事終了
堀北真希 左のフォークをとりあげる思索した時間は、0秒3210000000000001E-6
新垣結衣 8回目食事終了
堀北真希 右のフォークをとりあげる思索した時間は、1秒21894861600000004
北川景子 左のフォークをとりあげる思索した時間は、0秒9820000000000003E-6
堀北真希 9回目の食事
北川景子 右のフォークをとりあげる思索した時間は、0秒9820000000000003E-6
北川景子 9回目の食事
堀北真希 9回目食事終了
剛力彩芽 左のフォークをとりあげる思索した時間は、0秒652E-6
北川景子 9回目食事終了
剛力彩芽 右のフォークをとりあげる思索した時間は、0秒780406594
剛力彩芽 9回目の食事
桐谷美玲 左のフォークをとりあげる思索した時間は、0秒9820000000000003E-6
桐谷美玲 右のフォークをとりあげる思索した時間は、0秒294E-6
桐谷美玲 10回目の食事
剛力彩芽 9回目食事終了
新垣結衣 左のフォークをとりあげる思索した時間は、0秒973E-6
新垣結衣 右のフォークをとりあげる思索した時間は、0秒652E-6
新垣結衣 9回目の食事
桐谷美玲 10回目食事終了
堀北真希 左のフォークをとりあげる思索した時間は、0秒3210000000000001E-6
新垣結衣 9回目食事終了
堀北真希 右のフォークをとりあげる思索した時間は、0秒475458529
堀北真希 10回目の食事
北川景子 左のフォークをとりあげる思索した時間は、0秒9820000000000003E-6
北川景子 右のフォークをとりあげる思索した時間は、0秒312E-6
北川景子 10回目の食事
堀北真希 10回目食事終了
剛力彩芽 左のフォークをとりあげる思索した時間は、0秒652E-6
北川景子 10回目食事終了
剛力彩芽 右のフォークをとりあげる思索した時間は、0秒34860347900000005
新垣結衣 左のフォークをとりあげる思索した時間は、0秒652E-6
剛力彩芽 10回目の食事
剛力彩芽 10回目食事終了
新垣結衣 右のフォークをとりあげる思索した時間は、1秒0036094330000000507
新垣結衣 10回目の食事
新垣結衣 10回目食事終了

堀北真希 が 10回食事するのに要した時間は、28秒6504769030000013でした。
新垣結衣 が 10回食事するのに要した時間は、30秒9585105100000035でした。
剛力彩芽 が 10回食事するのに要した時間は、30秒0028747960000004014でした。
北川景子 が 10回食事するのに要した時間は、28秒998852129000003でした。
桐谷美玲 が 10回食事するのに要した時間は、27秒3586783140000023でした。

(○・ω・)ノ——– I eat delicious spaghetti and am stuffed. ——–(^_^)

ちゃんと動きました。(^_^)

このプログラムでは Semaphore インスタンス生成時のコンストラクタ引数に哲学者の人数を割り当てました。

また、公平性も設定しました。

パーミット数の設定は同時アクセスは 2 スレッドまでなので 2 と設定して、acquire()、release() とパーミット取得数とパーミット解放数を 1 してもいいでしょう。

今回は、パーミット数の設定を 5 , acquire(2)、release(2) とパーミット取得数とパーミット解放数を 2 としました。

同時アクセス可能なのが 2 スレッドで左右のフォークをロックするのでこの問題の鍵となるフォークの数を考慮にいれました。(あまり意味の無いこだわり)

final Semaphore semaphore = new Semaphore(PHILOSOPHERS_NUMBER, true);

NetBeans のプロファイラでスレッドの状態を確認してみましょう。

1

5 個の ForkJoinPool.commonPool-worker-29 とかのスレッドが最大で二つ同時アクセスされているのが確認できます。

スレッド終了後にフォークのロック解放待ちが無い場合はすぐに新たなスレッドが実行されているのも確認できます。

Semaphore って本当に便利ですね!

さて、ここでちょっとしたことを試してみよう。

Semaphore の公平性の設定をしなかったらどうなるのか?

final Semaphore semaphore = new Semaphore(PHILOSOPHERS_NUMBER); // 公平性無し

では、プログラムを実行させてみます。

剛力彩芽 左のフォークをとりあげる思索した時間は、0秒0810000000000002E-5
堀北真希 左のフォークをとりあげる思索した時間は、0秒0810000000000002E-5
剛力彩芽 右のフォークをとりあげる思索した時間は、0秒9820000000000003E-6
堀北真希 右のフォークをとりあげる思索した時間は、0秒9820000000000003E-6
剛力彩芽 1回目の食事
堀北真希 1回目の食事
堀北真希 1回目食事終了
堀北真希 左のフォークをとりあげる思索した時間は、0秒313E-6
堀北真希 右のフォークをとりあげる思索した時間は、0秒652E-6
堀北真希 2回目の食事
剛力彩芽 1回目食事終了
剛力彩芽 左のフォークをとりあげる思索した時間は、0秒312E-6
剛力彩芽 右のフォークをとりあげる思索した時間は、0秒652E-6
剛力彩芽 2回目の食事
堀北真希 2回目食事終了
堀北真希 左のフォークをとりあげる思索した時間は、0秒312E-6
堀北真希 右のフォークをとりあげる思索した時間は、0秒652E-6
堀北真希 3回目の食事
剛力彩芽 2回目食事終了
剛力彩芽 左のフォークをとりあげる思索した時間は、0秒910000000000001E-7
剛力彩芽 右のフォークをとりあげる思索した時間は、0秒3210000000000001E-6
剛力彩芽 3回目の食事
堀北真希 3回目食事終了
堀北真希 左のフォークをとりあげる思索した時間は、0秒3210000000000001E-6
堀北真希 右のフォークをとりあげる思索した時間は、0秒6510000000000002E-6
堀北真希 4回目の食事
剛力彩芽 3回目食事終了
剛力彩芽 左のフォークをとりあげる思索した時間は、0秒910000000000001E-7
剛力彩芽 右のフォークをとりあげる思索した時間は、0秒3210000000000001E-6
剛力彩芽 4回目の食事
堀北真希 4回目食事終了
堀北真希 左のフォークをとりあげる思索した時間は、0秒9820000000000003E-6
堀北真希 右のフォークをとりあげる思索した時間は、0秒652E-6
堀北真希 5回目の食事
剛力彩芽 4回目食事終了
剛力彩芽 左のフォークをとりあげる思索した時間は、0秒6420000000000003E-6
剛力彩芽 右のフォークをとりあげる思索した時間は、0秒6510000000000002E-6
剛力彩芽 5回目の食事
堀北真希 5回目食事終了
堀北真希 左のフォークをとりあげる思索した時間は、0秒312E-6
堀北真希 右のフォークをとりあげる思索した時間は、0秒3210000000000001E-6
堀北真希 6回目の食事
剛力彩芽 5回目食事終了
剛力彩芽 左のフォークをとりあげる思索した時間は、0秒910000000000001E-7
剛力彩芽 右のフォークをとりあげる思索した時間は、0秒652E-6
剛力彩芽 6回目の食事
剛力彩芽 6回目食事終了
剛力彩芽 左のフォークをとりあげる思索した時間は、0秒312E-6
剛力彩芽 右のフォークをとりあげる思索した時間は、0秒6510000000000002E-6
剛力彩芽 7回目の食事
堀北真希 6回目食事終了
堀北真希 左のフォークをとりあげる思索した時間は、0秒910000000000001E-7
堀北真希 右のフォークをとりあげる思索した時間は、0秒652E-6
堀北真希 7回目の食事
剛力彩芽 7回目食事終了
剛力彩芽 左のフォークをとりあげる思索した時間は、0秒6510000000000002E-6
剛力彩芽 右のフォークをとりあげる思索した時間は、0秒6510000000000002E-6
剛力彩芽 8回目の食事
堀北真希 7回目食事終了
堀北真希 左のフォークをとりあげる思索した時間は、0秒910000000000001E-7
堀北真希 右のフォークをとりあげる思索した時間は、0秒652E-6
堀北真希 8回目の食事
剛力彩芽 8回目食事終了
剛力彩芽 左のフォークをとりあげる思索した時間は、0秒3210000000000001E-6
剛力彩芽 右のフォークをとりあげる思索した時間は、0秒973E-6
剛力彩芽 9回目の食事
堀北真希 8回目食事終了
堀北真希 左のフォークをとりあげる思索した時間は、0秒6430000000000004E-6
堀北真希 右のフォークをとりあげる思索した時間は、0秒3210000000000001E-6
堀北真希 9回目の食事
剛力彩芽 9回目食事終了
剛力彩芽 左のフォークをとりあげる思索した時間は、0秒652E-6
剛力彩芽 右のフォークをとりあげる思索した時間は、0秒652E-6
剛力彩芽 10回目の食事
堀北真希 9回目食事終了
堀北真希 左のフォークをとりあげる思索した時間は、0秒3210000000000001E-6
堀北真希 右のフォークをとりあげる思索した時間は、0秒652E-6
堀北真希 10回目の食事
剛力彩芽 10回目食事終了
桐谷美玲 左のフォークをとりあげる思索した時間は、0秒312E-6
堀北真希 10回目食事終了
北川景子 左のフォークをとりあげる思索した時間は、0秒616E-6
桐谷美玲 右のフォークをとりあげる思索した時間は、0秒830071894
桐谷美玲 1回目の食事
桐谷美玲 1回目食事終了
北川景子 右のフォークをとりあげる思索した時間は、0秒888391644
北川景子 1回目の食事
北川景子 1回目食事終了
北川景子 左のフォークをとりあげる思索した時間は、0秒6420000000000003E-6
桐谷美玲 左のフォークをとりあげる思索した時間は、1秒44702734600000005
桐谷美玲 右のフォークをとりあげる思索した時間は、0秒652E-6
桐谷美玲 2回目の食事
桐谷美玲 2回目食事終了
北川景子 右のフォークをとりあげる思索した時間は、1秒2932113780000001
北川景子 2回目の食事
北川景子 2回目食事終了
北川景子 左のフォークをとりあげる思索した時間は、0秒3030000000000004E-6
桐谷美玲 左のフォークをとりあげる思索した時間は、0秒6195947270000001
桐谷美玲 右のフォークをとりあげる思索した時間は、0秒312E-6
桐谷美玲 3回目の食事
桐谷美玲 3回目食事終了
北川景子 右のフォークをとりあげる思索した時間は、1秒23741232
北川景子 3回目の食事
北川景子 3回目食事終了
北川景子 左のフォークをとりあげる思索した時間は、0秒6420000000000003E-6
桐谷美玲 左のフォークをとりあげる思索した時間は、1秒2788060170000002
桐谷美玲 右のフォークをとりあげる思索した時間は、0秒652E-6
桐谷美玲 4回目の食事
桐谷美玲 4回目食事終了
北川景子 右のフォークをとりあげる思索した時間は、0秒9319671190000001
北川景子 4回目の食事
北川景子 4回目食事終了
北川景子 左のフォークをとりあげる思索した時間は、0秒633E-6
桐谷美玲 左のフォークをとりあげる思索した時間は、0秒55758825
桐谷美玲 右のフォークをとりあげる思索した時間は、0秒3543E-5
桐谷美玲 5回目の食事
桐谷美玲 5回目食事終了
北川景子 右のフォークをとりあげる思索した時間は、0秒7469512070000001
北川景子 5回目の食事
北川景子 5回目食事終了
北川景子 左のフォークをとりあげる思索した時間は、0秒910000000000001E-7
桐谷美玲 左のフォークをとりあげる思索した時間は、1秒08432603500000013
桐谷美玲 右のフォークをとりあげる思索した時間は、0秒973E-6
桐谷美玲 6回目の食事
桐谷美玲 6回目食事終了
北川景子 右のフォークをとりあげる思索した時間は、0秒733817256
北川景子 6回目の食事
北川景子 6回目食事終了
北川景子 左のフォークをとりあげる思索した時間は、0秒9820000000000003E-6
桐谷美玲 左のフォークをとりあげる思索した時間は、1秒1698336330000001
桐谷美玲 右のフォークをとりあげる思索した時間は、0秒312E-6
桐谷美玲 7回目の食事
桐谷美玲 7回目食事終了
北川景子 右のフォークをとりあげる思索した時間は、1秒4533695360000001
北川景子 7回目の食事
北川景子 7回目食事終了
北川景子 左のフォークをとりあげる思索した時間は、0秒322E-6
桐谷美玲 左のフォークをとりあげる思索した時間は、0秒8525430700000001
桐谷美玲 右のフォークをとりあげる思索した時間は、0秒652E-6
桐谷美玲 8回目の食事
桐谷美玲 8回目食事終了
北川景子 右のフォークをとりあげる思索した時間は、0秒669598278
北川景子 8回目の食事
北川景子 8回目食事終了
北川景子 左のフォークをとりあげる思索した時間は、0秒9820000000000003E-6
桐谷美玲 左のフォークをとりあげる思索した時間は、1秒3454052980000002
桐谷美玲 右のフォークをとりあげる思索した時間は、0秒9820000000000003E-6
桐谷美玲 9回目の食事
桐谷美玲 9回目食事終了
北川景子 右のフォークをとりあげる思索した時間は、1秒25630280000000005
北川景子 9回目の食事
北川景子 9回目食事終了
北川景子 左のフォークをとりあげる思索した時間は、0秒652E-6
桐谷美玲 左のフォークをとりあげる思索した時間は、0秒598060677
桐谷美玲 右のフォークをとりあげる思索した時間は、0秒6510000000000002E-6
桐谷美玲 10回目の食事
桐谷美玲 10回目食事終了
北川景子 右のフォークをとりあげる思索した時間は、1秒09184714699999996
北川景子 10回目の食事
新垣結衣 左のフォークをとりあげる思索した時間は、0秒946E-6
新垣結衣 右のフォークをとりあげる思索した時間は、0秒312E-6
新垣結衣 1回目の食事
北川景子 10回目食事終了
新垣結衣 1回目食事終了
新垣結衣 左のフォークをとりあげる思索した時間は、0秒973E-6
新垣結衣 右のフォークをとりあげる思索した時間は、0秒6510000000000002E-6
新垣結衣 2回目の食事
新垣結衣 2回目食事終了
新垣結衣 左のフォークをとりあげる思索した時間は、0秒6430000000000004E-6
新垣結衣 右のフォークをとりあげる思索した時間は、0秒6510000000000002E-6
新垣結衣 3回目の食事
新垣結衣 3回目食事終了
新垣結衣 左のフォークをとりあげる思索した時間は、0秒312E-6
新垣結衣 右のフォークをとりあげる思索した時間は、0秒652E-6
新垣結衣 4回目の食事
新垣結衣 4回目食事終了
新垣結衣 左のフォークをとりあげる思索した時間は、0秒322E-6
新垣結衣 右のフォークをとりあげる思索した時間は、0秒6510000000000002E-6
新垣結衣 5回目の食事
新垣結衣 5回目食事終了
新垣結衣 左のフォークをとりあげる思索した時間は、0秒024E-5
新垣結衣 右のフォークをとりあげる思索した時間は、0秒6510000000000002E-6
新垣結衣 6回目の食事
新垣結衣 6回目食事終了
新垣結衣 左のフォークをとりあげる思索した時間は、0秒973E-6
新垣結衣 右のフォークをとりあげる思索した時間は、0秒3210000000000001E-6
新垣結衣 7回目の食事
新垣結衣 7回目食事終了
新垣結衣 左のフォークをとりあげる思索した時間は、0秒6430000000000004E-6
新垣結衣 右のフォークをとりあげる思索した時間は、0秒9820000000000003E-6
新垣結衣 8回目の食事
新垣結衣 8回目食事終了
新垣結衣 左のフォークをとりあげる思索した時間は、0秒652E-6
新垣結衣 右のフォークをとりあげる思索した時間は、0秒3210000000000001E-6
新垣結衣 9回目の食事
新垣結衣 9回目食事終了
新垣結衣 左のフォークをとりあげる思索した時間は、0秒910000000000001E-7
新垣結衣 右のフォークをとりあげる思索した時間は、0秒6510000000000002E-6
新垣結衣 10回目の食事
新垣結衣 10回目食事終了

堀北真希 が 10回食事するのに要した時間は、10秒9469584730000005でした。
新垣結衣 が 10回食事するのに要した時間は、40秒20581136300000225でした。
剛力彩芽 が 10回食事するのに要した時間は、10秒11629695400000095でした。
北川景子 が 10回食事するのに要した時間は、31秒21609595200000342でした。
桐谷美玲 が 10回食事するのに要した時間は、30秒20136357100000168でした。

(○・ω・)ノ——– I eat delicious spaghetti and am stuffed. ——–(^_^)

うわぁぁぁ・・・ 思いっきり不公平じゃないですか!

NetBeans のプロファイラでスレッドの状態を確認してみます。

2

今回のようなフォークのロック解放待ちがあるような場合はまずいような・・・

こんなとこもあるよね。

3

プログラムの処理時間が大幅に増えてます。

公平性の設定はよく考えないと思わぬ結果をもたらすかもしれませんね。

CompletableFuture を使えば楽に並行処理を記述できるし、Java って世に出たときはマルチスレッド対応だぜ!って誇らしげに売りにしていた。

あれから20年、確実に進化しています。

どんどん進化していく並行処理を日本語で優しく解説しているサイトはあまり無いのが残念です。

便利なものほど正しく使わないと痛い目をみますからね。

もっと正しい知識を優しい日本語の解説で入手したい今日この頃です。

今月の私は健康診断でコレステロールと中性脂肪の値が高いと指摘されたにもかかわらずこういった美味しいケーキをたくさん食べてしまいました。

c4

c3

c2

c5

美味しさそのままで中性脂肪とコレステロールの値をさげるケーキって無いものか。

お終い!

Hatena タグ:

Semaphore で遊ぶ

Java NetBeans

早いもので今日は五月五日 こどもの日です。

祝日法2条によれば、「こどもの人格を重んじ、こどもの幸福をはかるとともに、母に感謝する」となっています。

5月5日は古来から端午の節句として日本では男子の健やかな成長を祈願し各種の行事を行う風習があります。

ということで、私も男子の健やかな成長を祈願するプログラムを組むことにしました。

今さらですが、J2SE5.0 で追加された java.util.concurrent.Semaphore をつかってふざけた 夢のようなプログラム組みました。

男子の健やかな成長には素敵な女性との出会いは必要不可欠です。

だから4人の素敵な女性にデートに誘われると言う内容です。

しかし、体は一つしかありませんので一度に4人では収集付かなくなります。

そこで恭平さんにヘルプをお願いしました。(^_^;)

これでも、素敵な女性4人に対して男子は二人です。

これでなんとかするために Semaphore を使って二組のカップルまでがデートに出かけられるとします。

残りの二人の女性は先に出かけた二組のカップルのどちらか片方が帰ってきたらデートに出かけるという設定です。

つまり、男子二人というリソースに同時アクセス出来るのは素敵な女性スレッド二つまでということになります。

それが Semaphore を使えば実現できてしまうのです。

Semaphore はざっくり説明するとリソースに対するアクセスが可能か不可能かのいずれかの値をとる Binary Semaphore と

アクセス可能なリソース数を任意に設定することが出来る Counting Semaphore があります。

Java で採用された Semaphore は Counting Semaphore にあたります。

有限リソースに対して並行アクセス可能なスレッド数を自由に設定できます。

今回組むプログラムにうってつけの優れものとなっています。(^_^)

17 行目で Semaphore を生成しています。

コンストラクタの引数はパーミット数と公平性を設定します。

今回のプログラムではパーミット数はリソース数と同じ 2 となっています。

公平性は API ドキュメントを見ると今回のプログラムの場合は優しい私はパフォーマンスより平等性を重視して true と設定しました。

ちなみに API ドキュメントによると

このクラスのコンストラクタは、オプションで公平性パラメータを受け入れます。
falseに設定すると、このクラスはスレッドがパーミットを取得する順序について保証しません。
特に、バージ(barging)が許可されています。
つまり、acquire()を呼び出すスレッドに、待機していたスレッドより先にパーミットを割り当てることができます。
論理的には、新しいスレッドが、待機中のスレッドのキューの先頭に配置されます。
公平性がtrueに設定されると、セマフォは、acquireメソッドのいずれかを呼び出すスレッドが、これらのメソッドの呼出しが処理された順序(先入れ先出し、FIFO)でパーミットを取得するように選択されることを保証します。
FIFO順序付けは、必然的にこれらのメソッド内の特定の内部実行ポイントに適用されます。
そのため、あるスレッドが別のスレッドより前にacquireを呼び出しても、そのスレッドよりあとに順序付けポイントに到達する可能性があります。
また、メソッドからの復帰時も同様です。また、時間指定のないtryAcquireメソッドは公平性の設定に従いませんが、利用可能なパーミットをすべて取得することにも注意してください。

となっています。

実は今回のプログラムではこの公平性の設定の違いというのは解りにくいのですが

食事する哲学者の問題Dining Philosophers Problem)を Semaphore を使った簡易的なプログラムを組んでみるとよく解ります。

次に Semaphore からパーミットを取得するために acquire() メソッドを使います。

このプログラムでは 23 行目で使っています。

public void acquire() throws InterruptedException は API ドキュメントによると次のようになっています。

このセマフォからパーミットを取得します。パーミットが利用可能になるか、またはスレッドが割り込みされるまでブロックします。
パーミットが利用可能な場合はパーミットを取得してすぐに復帰するため、利用可能なパーミットの数は1つずつ減ります。

パーミットが利用可能でない場合、現在のスレッドはスレッドのスケジューリングに関して無効になり、次の2つのいずれかが起きるまで待機します。
•ほかのスレッドがこのセマフォに対してrelease()メソッドを呼び出し、現在のスレッドが次にパーミットを割り当てられるスレッドになる。
•ほかのスレッドが現在のスレッドに割り込みを行う。

現在のスレッドで、
•このメソッドへのエントリ上で設定された割込みステータスが保持されるか、
•パーミットの待機中に割り込みが発生した場合、
InterruptedExceptionがスローされ、現在のスレッドの割込みステータスがクリアされます。例外:InterruptedException – 現在のスレッドで割込みが発生した場合

これとは別に引数で取得するパーミット数を設定できるメソッドもあります。

次は取得したパーミットを解放する release() メソッドを調べてみます。

33 行目で使っています。

デートが終了したらパーミットを保持する必要はないので解放します。

public void release() は API ドキュメントによると次のように書かれています。

パーミットを解放し、セマフォに戻します。
パーミットを解放すると、利用可能なパーミットの数が1つずつ増えます。
いくつかのスレッドがパーミットを取得しようと試みている場合は、その中の1つのスレッドが選択され、解放されたばかりのパーミットが与えられます。
そのスレッドは、スレッドのスケジューリングに関して(ふたたび)有効になります。

パーミットを解放するスレッドは、acquire()の呼出しでそのパーミットを取得している必要はありません。
セマフォの適切な使用法は、アプリケーションでのプログラミング規約で確立されます。

たったこれだけでリソースにアクセスできる並行スレッド数を制限できてしまうんですねぇ・・・

このプログラムの実行結果は次のようになります。

待っているのは、堀北真希
待っているのは、北川景子
待っているのは、新垣結衣
待っているのは、剛力彩芽
今からデートに行くのは、北川景子
今からデートに行くのは、堀北真希
北川景子 は ゆっち とデートに行きました。
堀北真希 は 恭平 とデートに行きました。
北川景子 は ゆっち とデートから帰ってきました。
今からデートに行くのは、新垣結衣
新垣結衣 は ゆっち とデートに行きました。
新垣結衣 は ゆっち とデートから帰ってきました。
今からデートに行くのは、剛力彩芽
剛力彩芽 は ゆっち とデートに行きました。
剛力彩芽 は ゆっち とデートから帰ってきました。
堀北真希 は 恭平 とデートから帰ってきました。
(○・ω・)ノ——– Are you happy? ——–

さて、疑い深い私は NetBeans のプロファイラでスレッドの状態を確認してみました。( 注意:先ほどの実行結果とはことなります。)

1a

期待通りの動きをしてくれてます。(^_^) NetBeans のプロファイア手軽に使えて便利だね。

Semaphore 使える良い娘じゃないか!

もし、あなたが 恭平さん に素敵な女性とデートさせずに全ての女性を独り占めしたいとしたらこのプログラムをどのように変更したらいいでしょうか?

答えは簡単ですね!

私はヘルプを頼んだ 恭平さん に悪いのでプログラムの変更はいたしません。

どうしてもというかたはご自由に。(^_^;)

ちなみに今回のプログラムでは

CompletableFuture<Void> lovers1 = CompletableFuture.runAsync(() -> maki.play(), executor);

として ExecutorService を使いましたが

CompletableFuture<Void> lovers1 = CompletableFuture.runAsync(() -> maki.play());

として Fork/Join Framework を使うのもありです。

こどもの日の休日スペシャルはこれでお終いです。

食事する哲学者の問題Dining Philosophers Problem)に続く かも?

Hatena タグ: ,

「Javaのプログラムはどうやって動いているの?」古いパズルを試した。

Java

【初学者向け】JJUG ナイトセミナ 「Javaのプログラムはどうやって動いているの?」

https://www.youtube.com/watch?t=23&v=QNJBcrSayME

これを見てずっと前にさっぱり理解できなかったパズルがあって今なら少しだけなら解るような気がするかもしれないと思い試してみた。

元ネタは JAVA PUZZLERS の優れたパズラー パズル44:授業をサボる(Cutting Class)です。

このパズルを元に試してみたのですが時既に遅し・・・

Java SE 8 では既にパズルにはならなくなっていました。(Java SE 6 から)

なので、J2SE5.0 を使うことにしました。(ヲヒ

今回このパズルを試すのに組んだコードはこれです。

a

Love クラスはコンパイル時には存在していてちゃんとコンパイルされるものとします。

ただし、このプログラムの実行時には Love クラスは消えていることとします。

さて、どういった実行結果になるか想像できますか?

Lost Love が表示されるように思われますが残念な結果になります。

衝撃の事実をご覧ください。

3

なんでこうなるの?

バイトコードを覗いてみます。

4

バイトコードの怪しそうなところを見ていきましょう。

11: astore_1 とmain()メソッドのコードにあるのが問題の原因です。

これは catch ブロックでキャッチされたキャッチパラメータ e を VM 変数 1 に保存しています。

既に 7: astore_1 と Love クラスのローカル変数 love が VM 変数1に保存されています。

VM 変数1に上述の二つがマッピングされています。

このプログラムを実行すると JVM が起動、そしてクラスのロード、リンク、初期化、main 実行となります。

このプログラムはリンクのベリフィケーションで例外が発生してしまっています。

VM 変数1にマッピングされている二つのクラスをベリファイアはマージしようとする時にベリファイアは Love クラスのスーパークラスを決定するために Love クラスをロードしようとします。

しかし、 Love クラスは消えているのでロードに失敗してしまって NoClassDefFoundError がスローされます。

悲しいことにこの例外はベリフィケーション中にスローされていますのでクラスの初期化される前、main実行開始前ということでこのような残念なことになってしまいます。

本当に VM 変数1へのマッピングだけが問題かどうか確かめてみます。

プログラムを次のように変更します。

Love クラスのローカル変数の宣言を try – catch ブロックの外に出します。

b

プログラムを実行させて確認します。

c1

期待通りの動作となって Lost Love と出力されています。

キャッチパラメータ e も VM 変数 2 にマッピングされています。

11: astore_2

これでリンクのベリフィケーションも解決されて初期化、main実行で catch ブロックが実行されました。

このエントリーの冒頭にも記述しましたが現在の Java ではこのような問題は発生しません。

おそらくリンクの処理が賢くなってしまったんでしょう。

ついでだからプログラムのコードを戻して Java SE 6 で確認しておきます。

1

2

問題ないですね。

VM 変数1に Love クラスのローカル変数 love と catch ブロックでキャッチされたキャッチパラメータ e がマッピングされてもちゃんとベリフィケーションされるようになったようです。

JVM のことは詳しく解らないので間違っているかもしれませんがこうやってバイトコードを覗いてみるのもおもしろいですね。

例外テーブルの動作も表示されているし、BASIC のような goto 文まで。

詳しくは元ネタの本を読んでください。(^_^;)

今日の教訓:「Love が消えてしまうと悲しい!」

違う!

今日の教訓:「古い Java より新しい Java で幸せに!」

  

Hatena タグ:

« 古い記事 新しい記事 »