HeadFirstデザインパターン5章 チョコレートボイラSingleton

HeadFirstデザインパターン5章 は、Singletonパターンです。(架空の会社)Choc-O-Holic社の業務用チョコレートボイラを扱うためのコントローラクラスに、同期化していないSingletonを実装しました。

ところがマルチスレッドを導入したために、ボイラーが大変なことに!

マルチスレッドに対応するために、synchronizedバージョン、クラスロード時インスタンス化バージョン、ダブルチェックドロッキングバージョンを作っていきます。

githubの書籍サンプルコード/singleton

書籍のサンプルコードとは別に、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();
        }


        〜省略" ");
    }

ターミナルで実行すると、非同期バージョンは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

何がいけない?と思って調べると、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();
    }
}

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;
    }
}

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();
        }

    }
}

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();
    }
}

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();
    }
}

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();
    }
}

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();
    }
}

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();
    }
}
タイトルとURLをコピーしました