JavaのSingleton実装方法5つとメリットデメリット使用場面とかいろいろ書いてみる
こんにちは、G1GCを学んでからJVMにならクリーンされても構わないと思ったり思わなかったりするJava/Scala開発者です。
昨日久しぶりに真面目な技術関連以外の記事を書いたのですが、全くアクセスがなくて落ち込んでいます。
そこそこいい内容だと思ったんだけどな…世知辛いのじゃあ…
話変わって、最近めっきり寒くなってきましたね…そろそろバイク通勤が辛くなってきました。
今日も今日とて朝はとても暖かったのに、帰りはザーザーぶりで、死ぬ思いをしながらバイクを漕いで帰ってきました。
いや、本当に雨の日にバイクなんて乗るものじゃない。。。
さてさて、たまにはDesign Pattenの話をば。
Singletonパターンというデザインパターンはとても有名なので皆さんご存知かと思いますが、Javaにはいろいろと言語の仕様を使ったSingletonの書き方があるので、それを紹介していこうと思います。
メリットを説明するときに、オブジェクト指向上唯一でなくてはいけないインスタンスを生成できるとか、抽象的な説明をされる事が多いデザインパターンですね。
個人的には外部と通信したりミドルウェアアクセスしたりみたいな、重くて副作用の強い箇所を集めたうえでabstract factoryと組み合わせてFlyWeightチックに実装したりするときに使ったり、逆に完全なfunctionalな実装が必要な箇所で、Util系のクラスを使わずもっとオブジェクトチックな実装をするときに使うのが好きです。
Singletonパターンではないですが、Spring BeanのSingletonポリシーとかは前者の使い方に近いSingletonの使い方してるのかなという印象ですがどうでしょう。
(ここは結構まさかりが飛んできそうなので一旦見逃してください。もっといい使い方あるよという方はぜひコメントに!)
それでは本題!
ちなみに、コードのコメントを日本語で書くのがあまり好きではないので、コメントだけ英語にしてあります。どこかからパクってきたわけじゃないです。
普通のSingleton
class値にInstanceを持たせて、コンストラクタをprivateにすることで、外部クラスからのnewを禁止し、唯一性を担保する方法。
class NormalSingleton{ // Singleton instance. private static final NormalSingleton INSTANCE = new NormalSingleton(); // private constructor to prevent instantiation from other classes. private NormalSingleton() {} // static method to get the instance. public static NormalSingleton getInstance() { return INSTANCE; } }
みなさんご存知、一番普通な書き方ですね。軽いinstanceであればこれで十分です。instanceはstaticメンバなので、JVM起動時に読み込まれます。
INSTANCEをpublicにする書き方もあるらしいですが、個人的にはクラスメンバは直接参照されたくないので、public staticメソッドから呼び出します。
メリット
・実装がシンプルで簡単
・初学者にもわかりやすい
・スレッドセーフ
デメリット
・JVM起動時に必ずインスタンスが生成されるため重いインスタンスには不向き
・コンストラクタがリフレクションでアクセス可能なので厳密にはシングルトンではない
用途
汎用的な実装方法なのであえて挙げるのもあれですが、あえて挙げるなら
・学習
・サーバサイドで頻繁に使われることが事前に分かっているクラス
・重くはないがオブジェクト指向的な意味でSingletonにしたいクラス
あたりでしょうか?
遅延生成
初期値を持たせず、最初のgetInstanceメソッドの呼び出しで Instanceを作成し、二度目以降は同一のinstanceを使わせる方法。
class DelaySingleton { // field for singleton instance without any initial value (null) private static DelaySingleton instance; // private constructor to block the instantiation from other class. private DelaySingleton () {} // method to get the instance public static getInstance() { // create instance only in initial call of this method. if (instance == null) { instance = new DelaySingleton(); } return instance; } }
こちらもお馴染み、遅延生成するパターンです。少し重めのクラスなんかでよく使われます。実装は楽で明快なのでよく使われますが、実はこれ見ての通りThread Safeではないので、並列処理を行う環境では次に紹介するSynchronizedを使用してThreadSafeにしたパターンが使われます。
メリット
・実装がシンプルで簡単
・初学者にもわかりやすい
・重いクラスの生成を使用される直前まで遅延できる
デメリット
・非スレッドセーフ
・コンストラクタに加えinstanceまでfinalではないため、リフレクション経由で書き換え可能になり、よりSingletonではなくなる
用途
・学習
・マルチスレッドを使用しないバッチ処理など
SynchronizedでThreadSafe
インスタンスを生成する箇所を同期ブロックで囲み、強固にnewの呼び出しを一回に絞る方法。
class SynchronizedSingleton { // field for singleton instance without any initial value (null) private static SynchronizedSingleton instance; // private constructor to protect the instantiation from other class. private SynchronizedSingleton(){} // method to get the instance public static SynchronizedSingleton getInstance(){ // check if the instance is already instantiated. if(instance == null){ // block the multiple access from multiple thread synchronized (SynchronizedSingleton.class) { // check again, can be non-null, // since the instance already generated just at the process just before if(instance == null){ instance = new SynchronizedSingleton(); } } } return instance; } }
これもまた結構お馴染みかもしれません。デザインパターンの本や多くの入門記事で紹介されている方法です。ちなみにこの書き方、正直僕は嫌いです。
メリット
・スレッドセーフ
・重いインスタンスの生成を使用される直前まで遅延できる
デメリット
・最初の段階で大量にリクエストが来るとsynchronizedはパフォーマンス劣化につながる事がある
・コンストラクタに加えinstanceまでfinalではないため、リフレクション経由で書き換え可能になり、よりSingletonではなくなる
・冗長で書きづらい上に読みづらい
用途
・学習
・マルチスレッドを多用するバッチ
・tomcatなどで運用されるサーバサイドアプリケーション
Synchronized型のSingletonは結構問題が多いので、個人的には後述のBill Push Singletonを使うことが多いです。
Bill Push Singleton
インナークラスのクラス値は初回参照時までメモリに読み込まれないというJavaの言語仕様を利用したSingleton。
class BillPushSingleton{ // class to hold the instance as final private class SingletonHolder{ // this instance won't be loaded to memory until initial reference // normally, class value will be loaded into memory when the class is loaded to JVM with class loader. private static final BillPushSingleton INSTANCE = new BillPushSingleton(); } // private constructor to prevent instantiation from other classes private BillPushSingleton() {} // method to get the instance public static BillPushSingleton getInstance() { // this instance will be instantiated at the initial call only. return SingletonHolder.INSTANCE; } }
(僕の知る限り)日本ではあまり馴染みがない書き方かもしれませんが、海外の開発者が好んで使う書き方です。僕も簡素でエレガントだなと思うので、基本的に遅延生成のSingletonを作る場合はこの書き方を使います。
解説
一応解説すると、通常のクラスのクラス値はクラスローダがクラスをJVMに読み込んだ時点でインスタンス化され、ヒープのpermanent領域に保持されます。(permanent領域がわからない人はオライリーのJavaパフォーマンスを読もう!)
しかし、インナークラスに宣言された定数は、その親クラス自身が参照された段階ではnewされず、親クラスからそのインナークラスへの参照が発生した段階で初めてnewされてpermanent領域に保持されることになります。(実はこれ自体はクラスローダの仕様で、決して特殊なhackをしているわけではないのです。)
メリット
・実装がシンプルで簡単
・スレッドセーフ
・重いインスタンスの生成を使用される直前まで遅延できる
・Synchronizedによるパフォーマンス劣化の心配がない
デメリット
・リフレクション経由でインスタンス生成ができるため厳密にはSingletonではない
・JVMやクラスローダの知識がない人に説明するのがだるい(僕がこの記事を書いた一番大きな理由)
用途
・マルチスレッドを多用するバッチ
・tomcatなどで運用されるサーバサイドアプリケーション
ちなみにこれでもまだ厳密なSingletonではありません。コンストラクタが残ってしまっている以上、どうしても厳密なSingletonにはできないのです。
Enum Singleton
Enumはグローバルに唯一のインスタンスであるというJavaの言語仕様を利用したSingleton
enum EnumSingleton { // enum with only one element. // this element will act as same with instance of other impl pattern. INSTANCE("Now it is clear for you to see this is singleton, ahh??"); // Constructor to make this sample code looks like class. // this constructor is unable to be called even from reflection as Java lang spec. private EnumSingleton(String dummyMessage) { this.dummyMessage = dummyMessage; } // dummy filed to make this sample code look like the other impl pattern private String dummyMessage; // dummy method to show this enum can work like normal class. public String sampleMethod() { return "this method is sample to show this is not just the normal enum."; } }
こちらの実装方法は結構有名なのかもしれませんが、Javaの言語仕様としてEnumは厳密なSingletonであり、リフレクション経由でも新しいインスタンスを作ることはできません。
enumとして実装されたSingletonだけが、本当のSingletonなのです。ちなみに僕は普通にキモいなと思うので、知識として知っているだけで使った事がありません。
メリット
・厳密なSingleton
・スレッドセーフ
デメリット
・実装がキモい、enumにする意味がわからない
・インターフェイスや抽象クラスが使えず、他のデザインパターンと組み合わせにくい
・遅延生成ができない
・なぜそうしたかと問われるとうまく説明ができない
用途
・マルチスレッドを多用するバッチ
・tomcatなどで運用されるサーバサイドアプリケーション
・理想と現実の乖離に耐えられなくなって泣き出した開発者をなだめる
まとめ
色々と僕の知ってるSingletonパターンの実装方法を紹介してきました。個人的にはBill Push Singletonと普通のSingletonだけ使えればいいかなと思ってます。
ちなみにenum singletonをけちょんけちょんにいっていますが、それなりに伝統のある書き方なので、使ったからなんだということではないです。個人的な志向の話です。
それでは!