Javaの正しいシングルトン

各読者は「シングルトン」設計パターンが何であるかを知っていると確信していますが、誰もがそれを効率的かつ正しくプログラムする方法を知っているわけではありません。 この記事は、この問題に関する既存の知識を集約する試みです。



また、この記事は、以前にHabrahabrで公開された注目すべき研究の続きと考えることができます。



Javaのレイジーシングルトン


著者は、通常の初期化でテンプレートを実装する2つの方法を知っています。



1静的フィールド


public class Singleton { public static final Singleton INSTANCE = new Singleton(); }
      
      





+シンプルで透過的な実装

+スレッドセーフ

-遅延初期化なし



2列挙型シングルトン


Joshua Blochによれば、これはテンプレートを実装する最良の方法です[1]。



 public enum Singleton { INSTANCE; }
      
      





+機知に富んだ

+すぐに使用可能なシリアル化

+すぐに使えるスレッドセーフティ

+ EnumSet、EnumMapなどを使用する機能

+スイッチのサポート

-遅延初期化なし



Javaのレイジーシングルトン


執筆時点では、Javaの遅延初期化を使用したSingletonテンプレートの有効な実装が少なくとも3つあります。



1同期アクセサー


 public class Singleton { private static Singleton instance; public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
      
      





+遅延初期化

-最も典型的なアクセスでの低い生産性(クリティカルセクション)



2ダブルチェックロックおよび揮発性


 public class Singleton { private static volatile Singleton instance; public static Singleton getInstance() { Singleton localInstance = instance; if (localInstance == null) { synchronized (Singleton.class) { localInstance = instance; if (localInstance == null) { instance = localInstance = new Singleton(); } } } return localInstance; } }
      
      





+遅延初期化

+高性能

-JDK 1.5でのみサポート[5]



2.1なぜ揮発性なしでは機能しないのですか?


ダブルチェックロックのイディオムの問題は、Javaメモリモデル、より正確にはオブジェクトが作成された順序にあります。 次のステップ[2、3]で、この順序を条件付きで想像できます。



新しい学生を作成しましょう:Student s = new Student()、その後



1)local_ptr = malloc(sizeof(Student))//オブジェクト自体のメモリ割り当て。

2)s = local_ptr //ポインターの初期化;

3)学生:: ctor(s); //オブジェクトの構築(フィールドの初期化);



したがって、第2段階と第3段階の間で、別のスレッドが(ポインターがゼロでないという条件に基づいて)不完全に構築されたオブジェクトを受け取り、使用を開始できる状況が考えられます。 実際、この問題はJDK 1.5 [5]で部分的に解決されましたが、JSR-133 [5]の著者はDouble Chess Lockedに揮発性の使用を推奨しています。 さらに、そのようなことに対する彼らの態度は、仕様に関する解説から簡単にたどることができます。



スレッドが同期なしで通信できるようにするために提案されている、二重チェックロックイディオムなど、一般的ではあるが疑わしいコーディングイディオムが多数存在します。 そのようなイディオムのほとんどは、既存のセマンティクスでは無効であり、提案されているセマンティクスでは無効のままであると予想されます。


したがって、問題は解決されていますが、揮発性なしでダブルチェックロックを使用することは非常に危険です。 場合によっては、JVM、オペレーティング環境、スケジューラなどの実装によっては、このアプローチが機能しない場合があります。 ただし、JITが作成者によって生成したアセンブラコードを表示することを伴う一連の実験では、このようなケースは発生しませんでした。



最後に、不変オブジェクト(String、Integer、Floatなど)で例外なくDouble Checked Lockを使用できます。



3オンデマンドホルダーイディオム


 public class Singleton { public static class SingletonHolder { public static final Singleton HOLDER_INSTANCE = new Singleton(); } public static Singleton getInstance() { return SingletonHolder.HOLDER_INSTANCE; } }
      
      







+遅延初期化

+高性能

-非静的クラスフィールドには使用できません



性能


上記のメソッドのパフォーマンスを比較するために、マイクロベンチマークが使用されました[6]。これは、2つの並列スレッドからのシングルトンオブジェクトに対する1秒あたりの基本操作(フィールド増分)の数を決定します。



測定は、デュアルコアIntel Core 2 Duo T7300 2GHzマシン、2Gb ram、およびJava HotSpot(TM)Client VM(ビルド17.0-b17)で実行されました。 速度の単位では、1秒あたりの増分(したがって、オブジェクトキャプチャ)の数* 100,000が考慮されます。



(多いほど良い)

クライアント サーバー
同期アクセサー 42.6 86.3
ダブルチェックロックおよび揮発性 179.8 202.4
オンデマンドホルダー 181.6 202.7




結論:テンプレートの適切な実装を選択すると、 2xから4xへの加速(高速化)が得られます。



まとめ


このアプローチまたはそのアプローチを使用して「Loner」テンプレートを実装する際の次の短いヒントを区別できます[1]。



1)可能な限り通常の(レイジーではなく)初期化を使用します。

2)静的フィールドには、On Demand Holder idomを使用します。

3)単純なフィールドの場合は、ダブルチェッドロックとvolatile idomを使用します。

4)その他の場合はすべて、Syncronizedアクセサーを使用します。



Javaクラスライブラリとシングルトン


Javaクラスライブラリの開発者がテンプレートを実装する最も簡単な方法であるSyncronized Accessorを選択したことは注目に値します。 一方で、それは互換性と適切な動作の保証です。 一方、これは、各呼び出しでクリティカルセクションに出入りするプロセッサ時間の損失です。



ソースによるクイックgrep検索により、JCLにはそのような場所がたくさんあることが明らかになりました。



おそらく次の記事は、「すべてのシングルトンクラスがJavaクラスライブラリに正しく書き込まれたらどうなりますか?」:)



リンク集

[1] Joshua Bloch、Google I / O 2008での効果的なJava Reloadedトーク( ビデオ );

[2] ダブルチェックロックとシングルトンパターン

[3] 「二重チェックされたロックが壊れています」宣言

[4] en.wikipedia.org/wiki/Double-checked_locking

[5] JSR-133

[6] マイクロベンチマークの書き方



All Articles