シングルトンパターンとは?
シングルトンパターンとは、Javaを含むオブジェクト指向プログラミングで使用される
デザインパターンのひとつです。
クラスのインスタンスが唯一であることを保証したいときに使用されます。
複数のインスタンスを持たせたくない場合や、グローバルにアクセス可能な
オブジェクトを提供したい場合にとても役に立ちます。
特徴
シングルトンパターンの特徴はいくつかあります。
この中でも上位の2つは、シングルトンパターンを使う上で非常に大切な部分で、
この特徴をうまく使えないと、シングルトンパターンを使う意味がなくなるとも言えます。
唯一のインスタンス
シングルトンパターンを表現したクラスは、
インスタンスを1つしか持たないように設計されます。
これは、同じクラスのインスタンスが複数個存在すると問題がある場面に役立ちます。
例えば、設定情報やリソースの共有が必要な場面で役に立ちます。
グローバルなアクセス
シングルトンパターンは、クラスのインスタンスを1つしか持たないので、
グローバルにアクセスできるメソッドを提供します。
これにより、どこからでも同じインスタンスにアクセスでき、
特定のオブジェクトを共有することが可能となります。
一般的には、getInstanceメソッドを使用してインスタンスを取得します。
インスタンス生成の制御
クラスのコンストラクタをprivateにすることで、外部からのインスタンス生成を防ぎます。
つまり、クラス外からは新しくインスタンスを生成することが出来ず、
インスタンスが唯一であることを保証することができます。
スレッドセーフの考慮
シングルトンパターンは、マルチスレッド環境下で複数のスレッドが、
シングルトンインスタンスを取得しようとする場合に問題が発生することがあります。
そのため、シングルトンパターンを実装する際はスレッドセーフの考慮が必要になります。
これの解決策として、ダブルチェックロッキングやsynchronizedを使った手法が用いられます。
Enumによるシングルトンパターンの実装
Javaでは、Enumを使ってシングルトンパターンの実装をすることが推奨されることがあります。
これは、Enumの特性上、インスタンスが1つしか生成されず、
シリアライズやリフレクション攻撃に対しても安全であることが多いためです。
実装方法
ここからは、実際にシングルトンパターンの実装をしてみたいと思います。
いくつか考慮しないといけない点があるので、順番に解説をしていきます。
基本的な実装
まずは、一番シンプルな形で実装してみたいと思います。
public class MySingleton {
// 唯一のインスタンスを保持
private static MySingleton instance;
// privateなコンストラクタを定義(外部からのインスタンス生成を制御)
private MySingleton() {
System.out.println("インスタンスを生成しました。");
};
// インスタンスにアクセスするためのメソッド
public static MySingleton getInstance() {
if (instance == null) {
instance = new MySingleton();
}
return instance;
}
}
このように実装することで、初回アクセス時にインスタンスが生成されて、
唯一のインスタンスとなります。
アクセスするには、このようにします。
public class Sample1 {
public static void main(String[] args) {
MySingleton.getInstance();
}
}
スレッドセーフの考慮をした実装
上記のサンプルコードはシンプルで分かりやすいですが、
マルチスレッド環境下では、問題が発生してしまう場合があります。
そのため、つぎはスレッドセーフの考慮をした実装をしてみます。
public class MySingleton {
// 唯一のインスタンスを保持
private static MySingleton instance;
// privateなコンストラクタを定義(外部からのインスタンス生成を制御)
private MySingleton() {
System.out.println("インスタンスを生成しました。");
};
// インスタンスにアクセスするためのメソッド
public static synchronized MySingleton getInstance() {
if (instance == null) {
instance = new MySingleton();
}
return instance;
}
}
スレッドセーフの考慮はすごく簡単で、インスタンスの呼び出しをするメソッドに
synchronizedキーワードを付けるだけです。
これにより、同期が行われ、マルチスレッド環境下でも安全に動作します。
ダブルチェックロッキングを行った実装
先ほどのsynchronizedキーワードによる同期化は、パフォーマンスに影響を与えます。
そのため、今回はパフォーマンス改善をするために
ダブルチェックロッキングという手法で解決してみます。
public class MySingleton {
// 唯一のインスタンスを保持
private static volatile MySingleton instance;
// privateなコンストラクタを定義(外部からのインスタンス生成を制御)
private MySingleton() {
System.out.println("インスタンスを生成しました。");
};
// インスタンスにアクセスするためのメソッド
public static MySingleton getInstance() {
if (instance == null) {
synchronized (MySingleton.class) {
if (instance == null) {
instance = new MySingleton();
}
}
}
return instance;
}
}
先ほどよりは、少し複雑になりましたので、解説をしていきます。
volatileキーワード
まずは、volatile(ボラタイル)キーワードについてです。
サンプルコードでは、ここで記述しています。
private static volatile MySingleton instance;
volatileキーワードを変数につけると、常に最新の値が各スレッドから見えるようになります。
マルチスレッド環境下では、パフォーマンス改善のために、
各スレッドがフィールドの値をキャッシュすることがあります。
volatileは、このキャッシュを対象外にする役割をしています。
ダブルチェックロッキング
インスタンス生成時の処理を2段階でチェックをすることを、
ダブルチェックロッキングと言います。
今回のサンプルコードでは、getInstanceメソッドの中で行っています。
public static MySingleton getInstance() {
if (instance == null) {
synchronized (MySingleton.class) {
if (instance == null) {
instance = new MySingleton();
}
}
}
return instance;
}
最初のif文では、インスタンスが既に生成されているかの確認をしています。
ここでインスタンスが生成されていれば後続の処理は不要になります。
まだ、インスタンスが生成されていない(instance=null)の場合は、
synchronizedブロックに入り、スレッドセーフにインスタンス生成がされます。
さらに、synchronizedブロック内で再度if文でインスタンス生成の有無を確認します。
これは、複数のスレッドが同時にgetInstanceメソッドを呼んだ場合に、
2回目以降のインスタンス生成を防ぐためです。
synchronizedブロックの最小化
synchronizedは高コストな処理になります。
よって、パフォーマンスに影響を与えてしまうことがあります。
そのため、最小限の範囲で同期処理を行うことが推奨されています。
今回のサンプルコードでは、インスタンス生成がまだの場合のみ
synchronizedブロックに入るため、初回以降は、ほとんど同期処理が発生せず、
パフォーマンスに影響を与えづらくしています。
Enumを使った実装
シングルトンパターンの実装をシンプル且つ、スレッドセーフに実装する方法として、
Enumを使った実装が存在します。
Enumは、スレッドセーフであり、インスタンスが1つであることが保証されています。
public enum MyEnumSingleton {
INSTANCE;
public void doSomething() {
System.out.println("OK");
}
}
呼び出し方は、以下のようになります。
public class Sample1 {
public static void main(String[] args) {
MyEnumSingleton instance = MyEnumSingleton.INSTANCE;
instance.doSomething();
}
}
また、Enumについては、以下の記事でも解説していますので、
気になる方は、参考にしてください。
よく使われる状況
シングルトンパターンが採用される状況は、いろいろありますが、
以下のような状況ではよく使われていると思います。
設定情報の管理
アプリ全体で共有される設定情報はを管理するクラスにはシングルトンパターンがよく採用されます。
設定情報は、アプリ全体で一貫性を持たせる必要があるため、
インスタンスを1つに限定させる必要がります。
ログ管理
ログ出力を行うクラスにも、シングルトンパターンがよく採用されます。
複数のインスタンスがログファイルを使うと、ログが散在し、管理が困難になります。
そのため、インスタンスを1つに限定し、一元管理することができる
シングルトンパターンが採用されます。
リソース管理
限られたリソース(データベース接続など)を共有・再利用する場合、
シングルトンパターンが採用されます。
リソースの生成と破棄を一元管理することができ、効率的にリソースを活用することができます。
実践的なサンプルコード
ここでは、実際に使う場面を想定して、サンプルコードを作ってみます。
データベース接続情報を管理する
今回は、データベース接続情報を管理するクラスにシングルトンパターンを採用した例です。
まずは、シングルトンパターンを実装したクラスを作成します。
public class ConfigManager {
private static volatile ConfigManager instance;
// DB接続情報
private String url;
private String user;
private String pass;
private String apiKey;
// コンストラクタ
private ConfigManager() {
// 初期値
this.url = "url";
this.user = "test-user";
this.pass = "pass";
this.apiKey = "abc";
}
// シングルトンインスタンス取得用
public static ConfigManager getInstance() {
if (instance == null) {
synchronized (ConfigManager.class) {
if (instance == null) {
instance = new ConfigManager();
}
}
}
return instance;
}
// 設定情報取得用&更新用メソッド
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getUser() {
return user;
}
public void setUser(String user) {
this.user = user;
}
public String getPass() {
return pass;
}
public void setPass(String pass) {
this.pass = pass;
}
public String getApiKey() {
return apiKey;
}
public void setApiKey(String apiKey) {
this.apiKey = apiKey;
}
}
次は、Mainメソッドからアクセスしてみます。
public class Sample1 {
public static void main(String[] args) {
ConfigManager config = ConfigManager.getInstance();
// 情報の取得
System.out.println("url:" + config.getUrl());
System.out.println("user:" + config.getUser());
System.out.println("pass:" + config.getPass());
System.out.println("apiKey:" + config.getApiKey());
// 情報の更新
config.setUser("test-user2");
// 更新後の情報取得
System.out.println("user:" + config.getUser());
}
}
まとめ
ここまで読んでいただきありがとうございます。
今回は、シングルトンパターンについて解説しました。
少し複雑な内容でしたが、シングルトンパターンを使えば、プルグラムの幅が広がると思います。
サンプルコードを改造してより便利に使ってみてください。
コメント