HeadFirstデザインパターン5章 は、Singletonパターンです。(架空の会社)Choc-O-Holic社の業務用チョコレートボイラを扱うためのコントローラクラスに、同期化していないSingletonを実装しました。
ところがマルチスレッドを導入したために、ボイラーが大変なことに!
マルチスレッドに対応するために、synchronizedバージョン、クラスロード時インスタンス化バージョン、ダブルチェックドロッキングバージョンを作っていきます。
書籍のサンプルコードとは別に、1000スレッドを起動して、Singletonかどうかを確認するmainを作りました。
Main.java最初のバージョン
異なるインスタンスが何個生成されるか、HashMapで集計しました。
private static void simulator(ChocolateBoilerFactory.Type type) {
final int N_THREDS = 1000;
final CountDownLatch startGate = new CountDownLatch(1);
final CountDownLatch endGate = new CountDownLatch(N_THREDS);
final Map<Object,Integer> map = new HashMap<Object,Integer>();
for (int i = 0; i < threads.length; i += 1) {
final int n = i;
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
startGate.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
Object obj = ChocolateBoilerFactory.create(type);
map.put(obj, 1);
endGate.countDown();
}
});
thread.start();
}
〜省略〜
System.out.print(map.size() + " ");
}
Code language: PHP (php)
ターミナルで実行すると、非同期バージョンは100以上のインスタンスを生成しました。その他のバージョンのインスタンス数は、すべて1のはずですが、2や3がちらほら。
for((i=0; i < 5; i+=1)); do java headfirst.designpatterns.chap5.Main; done
2 3 1 1 1
5 3 1 1 1
5 5 1 3 1
4 3 1 1 2
2 4 1 1 1
Code language: HTML, XML (xml)
何がいけない?と思って調べると、map.entrySet().size()も1ではありませんでした。ところが、map.entrySet()やmap.keySet()をiterator()で表示してみると、内容自体は1個でした。
HashSetを使っても、2や3がちらほらありました。HashMapやHashSetはスレッドセーフではないことが原因なんですね。
HashMapをConcurrentHashMapに置き換えたところ、その他のバージョンのインスタンス数は、すべて1になりました。
スレッド同期化Singletonの実験をしているので、ConcurrentHashMapを使わないようにと考えて、最終的には、スレッド数と同じ長さの配列を用意して、最後にユニーク数を集計することにしました。
Main.java最終バージョン
package headfirst.designpatterns.chap5;
import java.util.*;
public class Main {
public static void main(String[] args) {
simulator(ChocolateBoilerFactory.Type.NO_SYNCHRONIZED);
simulator(ChocolateBoilerFactory.Type.SYNCHRONIZED);
simulator(ChocolateBoilerFactory.Type.INITILIZATION_ON_CLASS_LOADED);
simulator(ChocolateBoilerFactory.Type.INITILIZATION_ON_DEMAND_HOLDER);
simulator(ChocolateBoilerFactory.Type.DOUBLE_CHECKED_LOCKING);
System.out.println();
}
private static void simulator(ChocolateBoilerFactory.Type type) {
final int N_THREDS = 1000;
final CountDownLatch startGate = new CountDownLatch(1);
final CountDownLatch endGate = new CountDownLatch(N_THREDS);
final Object[] instances = new Object[N_THREDS];
for (int i = 0; i < N_THREDS; i += 1) {
final int n = i;
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
startGate.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
Object obj = ChocolateBoilerFactory.create(type);
instances[n] = obj;
endGate.countDown();
}
});
thread.start();
}
startGate.countDown();
try {
endGate.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
final int n_instances = unique_count(instances);
System.out.print(n_instances + " ");
}
private static int unique_count(Object[] instances) {
final Set<Object> set = new HashSet<Object>();
for (int i = 0; i < instances.length; i += 1) {
set.add(instances[i]);
}
return set.size();
}
}
Code language: JavaScript (javascript)
ChocolateBoiler.java
abstractにしたり、Factoryクラスを作っていますが、基本的なところは書籍サンプルのままです。今回のMainクラスでは、fill()やdrain()は呼んでいません。
package headfirst.designpatterns.chap5;
public abstract class ChocolateBoiler {
private boolean empty;
private boolean boiled;
public ChocolateBoiler() {
empty = true;
boiled = false;
}
public void fill() {
if (isEmpty()) {
empty = false;
boiled = false;
}
}
public void drain() {
if (!isEmpty() && isBoiled()) {
empty = true;
}
}
public void boil() {
if (!isEmpty() && !isBoiled()) {
boiled = true;
}
}
public boolean isEmpty() {
return empty;
}
public boolean isBoiled() {
return boiled;
}
}
Code language: PHP (php)
ChocolateBoilerFactory.java
package headfirst.designpatterns.chap5;
public class ChocolateBoilerFactory {
public enum Type {
NO_SYNCHRONIZED,
SYNCHRONIZED,
INITILIZATION_ON_CLASS_LOADED,
INITILIZATION_ON_DEMAND_HOLDER,
DOUBLE_CHECKED_LOCKING
};
public static ChocolateBoiler create(Type type) {
switch (type) {
case NO_SYNCHRONIZED:
return ChocolateBoilerNoSynchronized.getInstance();
case SYNCHRONIZED:
return ChocolateBoilerSynchronized.getInstance();
case INITILIZATION_ON_CLASS_LOADED:
return ChocolateBoilerInitializationOnClassLoaded.getInstance();
case INITILIZATION_ON_DEMAND_HOLDER:
return ChocolateBoilerInitializationOnDemandHolder.getInstance();
case DOUBLE_CHECKED_LOCKING:
return ChocolateBoilerDoubleCheckedLocking.getInstance();
default:
throw new IllegalArgumentException();
}
}
}
Code language: PHP (php)
ChocolateBoilerNoSynchronized.java
非同期バージョン
package headfirst.designpatterns.chap5;
public class ChocolateBoilerNoSynchronized extends ChocolateBoiler {
private static ChocolateBoilerNoSynchronized instance;
public static ChocolateBoilerNoSynchronized getInstance() {
if (instance == null) {
instance = new ChocolateBoilerNoSynchronized();
}
return instance;
}
private ChocolateBoilerNoSynchronized() {
super();
}
}
Code language: PHP (php)
ChocolateBoilerSynchronized.java
synchronizedバージョン
package headfirst.designpatterns.chap5;
public class ChocolateBoilerSynchronized extends ChocolateBoiler {
private static ChocolateBoilerSynchronized instance;
public synchronized static ChocolateBoilerSynchronized getInstance() {
if (instance == null) {
instance = new ChocolateBoilerSynchronized();
}
return instance;
}
private ChocolateBoilerSynchronized() {
super();
}
}
Code language: PHP (php)
ChocolateBoilerInitializationOnClassLoaded.java
クラスロード時インスタンス化バージョン
package headfirst.designpatterns.chap5;
public class ChocolateBoilerInitializationOnClassLoaded extends ChocolateBoiler {
private static final ChocolateBoilerInitializationOnClassLoaded instance = new ChocolateBoilerInitializationOnClassLoaded();
public static ChocolateBoilerInitializationOnClassLoaded getInstance() {
return instance;
}
private ChocolateBoilerInitializationOnClassLoaded() {
super();
}
}
Code language: PHP (php)
ChocolateBoilerInitializationOnDemandHolder.java
オンデマンドホルダーインスタンス化バージョン
package headfirst.designpatterns.chap5;
public class ChocolateBoilerInitializationOnDemandHolder extends ChocolateBoiler {
private static class SingletonHolder {
private static final ChocolateBoilerInitializationOnDemandHolder instance = new ChocolateBoilerInitializationOnDemandHolder();
}
public static ChocolateBoilerInitializationOnDemandHolder getInstance() {
return SingletonHolder.instance;
}
private ChocolateBoilerInitializationOnDemandHolder() {
super();
}
}
Code language: PHP (php)
ChocolateBoilerDoubleCheckedLocking.java
ダブルチェックドロッキングバージョン
package headfirst.designpatterns.chap5;
public class ChocolateBoilerDoubleCheckedLocking extends ChocolateBoiler {
private volatile static ChocolateBoilerDoubleCheckedLocking instance;
public static ChocolateBoilerDoubleCheckedLocking getInstance() {
if (instance == null) {
synchronized (ChocolateBoilerDoubleCheckedLocking.class) {
if (instance == null) {
instance = new ChocolateBoilerDoubleCheckedLocking();
}
}
}
return instance;
}
private ChocolateBoilerDoubleCheckedLocking() {
super();
}
}
Code language: PHP (php)