單例模式
- 1.概述
- 2.實作
- 2.1懶漢式
- 2.1.1代碼
- 2.1.2多執行緒下的問題
- 2.1.3 DoubleCheck雙重檢查
- 2.1.4反射攻擊解決方案
- 2.1.5序列化破壞單例模式分析及解決方案
- 2.2餓漢式
- 2.2.1代碼
- 2.2.2反射攻擊解決方案
- 2.3靜態內部類
- 2.3.1代碼
- 2.3.2反射攻擊解決方案
- 2.4列舉單例
- 2.4.1代碼
- 2.4.2原理剖析
- 2.4.2.1序列化測驗
- 2.4.2.2反射攻擊測驗
- 2.4.2.3 JAD反編譯
1.概述
單例模式(Singleton Pattern):確保某一個類只有一個實體,而且自行實體化并向整個系統提供這個實體,這個類稱為單例類,它提供全域訪問的方法,單例模式是一種創建型模式,是一種簡單實用又復雜的設計模式,
2.實作
懶漢式和餓漢式都是一種比較形象的稱謂,
- 懶漢式,既然比較懶,裝載類的時候不創建物件,會一直等到要用到物件實體的時候才會去創建,這種技術又稱為“延遲加載(
Lazy Load)技術”, - 餓漢式,既然比較餓,裝載物件的時候就去創建物件實體,
2.1懶漢式
- 私有化構造方法:要想在運行期間控制某一個類的實體只有一個,首要的任務就要控制創建實體的地方,即不能隨隨便便就可以實體化物件,我們可以將構造器私有化,這樣就能禁止類的外部直接使用new來創建物件,
private LazySingleton(){...}
- 提供獲取實體的方法:將建構式的可見性改為
private后,雖然外部不能再使用new來創建物件,但是在LazySingleton的內部還是可以創建物件的,同時為了讓外界訪問到這個唯一實體,可以提供一個方法來回傳類的實體,同時該方法要加上static,直接通過類來呼叫物件,因為這個方法是static的,那么屬性也要被迫變成static的(這里并沒有用到static的特性),
private static LazySingleton lazySingleton = null;
public static LazySingleton getInstance(){
if(lazySingleton == null){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
2.1.1代碼
public class LazySingleton {
private static LazySingleton lazySingleton = null;
private LazySingleton(){
}
public static LazySingleton getInstance(){
if(lazySingleton == null){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
2.1.2多執行緒下的問題
在單執行緒情況下,上面的代碼是沒有問題的,但在多執行緒的環境下,是不安全的,我們假設thread0和thread1同時來到了if(lazySingleton == null),因為此時lazySingleton是null,兩個執行緒都通過了條件判斷,開始new操作,這樣一來,lazySingleton就被實體化了兩次,

我們寫兩個類通過多執行緒debug的方式進行測驗,
public class T implements Runnable {
@Override
public void run() {
LazySingleton lazySingleton = LazySingleton.getInstance();
System.out.println(Thread.currentThread().getName()+" "+lazySingleton);
}
}
public class Test {
public static void main(String[] args) throws Exception{
Thread t1 = new Thread(new T());
Thread t2 = new Thread(new T());
t1.start();
t2.start();
System.out.println("program end");
}
}




由上圖可知,lazySingleton實體化了兩次,我們只需要在方法中加上synchronized即可,這樣當thread0進入getInstance()時,thread1就處于阻塞狀態,解決了執行緒安全問題,
public synchronized static LazySingleton getInstance(){...}
2.1.3 DoubleCheck雙重檢查
上面的代碼雖然解決了執行緒安全的問題,但是每次呼叫getInstance()時都需要進行執行緒鎖定判斷,在多執行緒高并發環境中,將會導致系統性能大大降低,事實上,上述代碼無須對整個getInstance()方法進行鎖定,只需要鎖定代碼lazySingleton = new LazySingleton();即可,同時要注意,因為這個方法是靜態方法,存在方法區并且整個JVM只有一份,所以要加類鎖,即synchronized (LazyDoubleCheckSingleton.class){...}
//LazyDoubleCheckSingleton 與上面LazySingleton作用一樣
public static LazyDoubleCheckSingleton getInstance(){
if(lazyDoubleCheckSingleton == null){
//類鎖
synchronized (LazyDoubleCheckSingleton.class){
lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
}
}
return lazyDoubleCheckSingleton;
}
上面代碼看似解決了問題,但事實并非如此,使用上面代碼來創建單例物件,還是會存在單例物件不唯一的情況,
假設某一瞬間,thread0和thread1同時來到了if(lazySingleton == null),因為此時lazySingleton是null,兩個執行緒都通過了條件判斷,由于實作了synchronized加鎖機制,thread0進入synchronized鎖定的代碼中執行操作,thread1處于阻塞狀態,必須等到執行緒A執行完畢后才能進入synchronized鎖定的代碼,但是thread0執行完,thread1并不知道實體已經創建完成,將會繼續創建實體,產生了多個單例物件,需要進一步改進,在synchronized鎖定的代碼里再進行一次if(lazySingleton == null),這種方式就稱為雙重檢查鎖定,同時由于JVM編譯器的指令重排機制,同樣會出現問題,這并不是百分百發生的,但既然存在安全隱患,我們就需要解決它,
當進行lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();時,JVM會進行下面的1,2,3操作(instance即lazyDoubleCheckSingleton),其中2,3的操作順序可能顛倒,
假設執行緒0要按照1,3,2的順序進行初始化物件,恰好在1,3步完成synchronized鎖釋放后,執行緒1進入synchronized代碼塊中,此時instance不為null,執行緒1比執行緒0首先訪問了未初始化好的物件,我們只需要在instance物件前面增加一個修飾符volatile,就可以始終保持1,2,3的初始化順序,這樣在執行緒1看來,instance物件的參考要么指向null,要么指向一個初始化完成的instance,從而保證了安全,完整代碼如下:
public class LazyDoubleCheckSingleton {
private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
private LazyDoubleCheckSingleton(){
}
public static LazyDoubleCheckSingleton getInstance(){
if(lazyDoubleCheckSingleton == null){
synchronized (LazyDoubleCheckSingleton.class){
if(lazyDoubleCheckSingleton == null){
lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
//1.分配記憶體給這個物件
//3.設定lazyDoubleCheckSingleton 指向剛分配的記憶體地址
//2.初始化物件
//intra-thread semantics
---------------//3.設定lazyDoubleCheckSingleton 指向剛分配的記憶體地址
}
}
}
return lazyDoubleCheckSingleton;
}
}
2.1.4反射攻擊解決方案
在Java語言中,不僅僅可以通過new關鍵字直接創建物件,還可以通過反射機制創建物件,比如我們可以通過反射獲取類中的屬性,方法,構造器,即使這些的訪問權限是private,我們也可以通過setAccessible()來啟動和禁用訪問安全檢查的開關,引數值為true則指示反射的物件在使用時應該取消Java語言訪問檢查,那么我們就可以對這個物件為所欲為了,
比如:
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
Constructor<LazySingleton> c = LazySingleton.class.getDeclaredConstructor();
//引數值為true則指示反射的物件在使用時應該取消Java語言訪問檢查
c.setAccessible(true);
LazySingleton instance1 = c.newInstance();
LazySingleton instance2 = LazySingleton.getInstance();
System.out.println(instance1);
System.out.println(instance2);
System.out.println(instance1 == instance2);
}
列印結果如下,很明顯這兩個物件是不一樣的,

我們可以在私有構造器里面進行判斷,如果lazySingleton物件已經實體化了,就拋出例外,
private LazySingleton(){
if (lazySingleton != null){
throw new RuntimeException("單例構造器禁止反射呼叫");
}
}
其實對于餓漢式這樣處理完全沒有問題,但是懶漢式是有問題的,比如我們先通過反射創建一個物件,創建后的lazySingleton還是null,這個時候還是可以通過getInstance()獲取lazySingleton物件的,這樣就兩個了,
其實我們還可以通過設定標志位的方式來解決這個問題,
public class LazySingleton {
private static LazySingleton lazySingleton = null;
private static boolean flag =true;
private LazySingleton(){
if (flag){
flag = false;
}else {
throw new RuntimeException("單例構造器禁止反射呼叫");
}
if (lazySingleton != null){
throw new RuntimeException("單例構造器禁止反射呼叫");
}
}
public synchronized static LazySingleton getInstance(){
if (lazySingleton == null){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
2.1.5序列化破壞單例模式分析及解決方案
public static void main(String[] args) throws Exception{
/**
* 序列化測驗
*/
LazySingleton instance = LazySingleton.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
oos.writeObject(instance);
File file = new File("singleton_file");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
LazySingleton newInstance = (LazySingleton) ois.readObject();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
}
當我們進行上面的序列化和反序列化操作時,每次反序列化一個序列化的實體時,都會創建一個新的實體,

所以當我們想將單例類變成可序列化的,僅僅在宣告上加上implements Serializable 是不夠的, 為了維護并保證單例,必須宣告所有實體域都是瞬時( transient )的,并提供一個 readResolve 方法,
[effective Java(第三版)]
readResolve 特性允許你用 readObject 創建的實體代替另一個實體[Serialization,3.7 ]對于一個正在被反序列化的物件,如果它的類定義了一個 readResolve 方法,并且具備正確的宣告,那么在反序列化之后,新建物件上的 readResolve 方法就會被呼叫,然后,該方法回傳的物件參考將被回傳,取代新建的物件 ,在這個特性的絕大多數用法中,指向新建物件的參考不需要再被保留,因此立即成為垃圾回收的物件,
序列化形式并不需要包含任何實際的資料;所有的實體域都應該被宣告為瞬時的 ,事實上,如果依賴 readResolve 進行實體控制,帶有對參考型別的所有實體域 必須 transient , 否則,那種破釜沉舟式的攻擊者,就有可能在readResolve 方法被運行之前,保護指向反序列化物件的參考
private static transient LazySingleton lazySingleton = null;
private Object readResolve(){
return lazySingleton;
}
我們只需要將代碼修改為上面的部分,就可以得到預期的結果,

通過對原始碼的簡單分析,其實作的原理為先通過反射創建一個反序列化后的物件,如果單例物件中定義了readResolve()方法,則對前面生成的物件進行覆寫,來保證單例,

2.2餓漢式
這個方案裝載物件的時候就去創建物件實體,在Java中,static有兩個特性:
static變數在類裝載的時候進行初始化- 多個實體的
static變數會共享同一塊記憶體區域
所以定義一個靜態變數來存盤創建好的類實體
private final static HungrySingleton hungrySingleton = new HungrySingleton();
因為餓漢式在類加載的時候就將自己實體化,無須考慮多執行緒訪問的問題,可以確保實體的唯一性,
2.2.1代碼
public class HungrySingleton implements Serializable {
private final static HungrySingleton hungrySingleton = new HungrySingleton();
private HungrySingleton(){
}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
}
2.2.2反射攻擊解決方案
同樣因為類加載的時候就將自己初始化好了,所以當反射攻擊的時候,只需要進行if (hungrySingleton != null)的操作即可,
private HungrySingleton(){
if (hungrySingleton != null){
throw new RuntimeException("單例構造器禁止反射呼叫");
}
}
2.3靜態內部類
餓漢式單例類不不能實作延遲加載,不管將來用不用,它始終占據記憶體;而懶漢式單例類安全控制繁瑣麻煩,而且性能也會受到影響,即將要介紹的這種方式,能夠將二者的缺點克服而兼顧優點,這種解決方案被稱為Lazy initialization holder class (IoDH)模式.
首先介紹一下靜態內部類的屬性
- 由
static修飾的成員式內部類,它的物件與外部類物件不發生依賴關系,其相當于其外部類的成員, - 外部類初次加載,會初始化靜態變數、靜態代碼塊、靜態方法,但不會加載內部類和靜態內部類,
- 呼叫靜態內部類的變數時,外部類并沒有加載,時間與外部類是否加載以及加載時間無關,靜態內部類只有被呼叫時才會被加載,從而實作了延遲加載,
由靜態內部類的屬性就可以知道,只要不使用這個靜態內部類,那就不會創建物件實體,從而實作了延遲加載和執行緒安全,
2.3.1代碼
public class StaticInnerClassSingleton {
private static class InnerClass{
private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
}
public static StaticInnerClassSingleton getInstance(){
return InnerClass.staticInnerClassSingleton;
}
private StaticInnerClassSingleton(){
}
}
當第一次呼叫getInstance()時,它第一次讀取InnerClass.staticInnerClassSingleton,導致InnerClass內部類得到初始化,而這個類在裝載并被初始化的時候,會初始化它的靜態域,從而創建了StaticInnerClassSingleton 的實體,由于是靜態的屬性,因此只會在虛擬機裝載類的時候初始化一次,并由JVM來保證它的執行緒安全性,
2.3.2反射攻擊解決方案
與餓漢式處理方式基本一致
private StaticInnerClassSingleton(){
if (InnerClass.staticInnerClassSingleton != null){
throw new RuntimeException("單例構造器禁止反射呼叫");
}
}
2.4列舉單例
在 《effective Java》第三版 中提到: 單元素列舉型別經常成為實作 Singleton的最佳方法, 由于在平時的開發程序中,列舉類用的并不多,所以提前總結一下列舉類的一些重要用法,
- 使用
enum定義的列舉類默認繼承了java.lang.Enum類,因此不能再繼承其他類 - 列舉類的構造器只能使用 private 權限修飾符
- 列舉類的所有實體必須在列舉類中顯式列出(, 分隔 ; 結尾),列出的實體系統會自動添加 public static final 修飾
- 必須在列舉類的第一行宣告列舉類物件
常用方法:
values():回傳列舉型別的物件陣列,該方法可以很方便地遍歷所有的列舉值,valueOf(String str):可以把一個字串轉為對應的列舉類物件,要求字串必須是列舉類物件的“名字”,如不是,會有運行時例外:IllegalArgumentException,toString():回傳當前列舉類物件常量的名稱
2.4.1代碼
public enum EnumInstance {
INSTANCE;
private Object data;
public void setData(Object data) {
this.data = data;
}
public Object getData() {
return data;
}
public static EnumInstance getInstance(){
return INSTANCE;
}
}
2.4.2原理剖析
2.4.2.1序列化測驗
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
/**
* 驗證 列舉類如何防止序列化創建物件
*/
EnumInstance instance = EnumInstance.getInstance();
instance.setData(new Object());
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
oos.writeObject(instance);
File file = new File("singleton_file");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
EnumInstance newInstance = (EnumInstance) ois.readObject();
System.out.println(instance.getData());
System.out.println(newInstance.getData());
System.out.println(instance.getData() == newInstance.getData());
}
結果:

通過結果可以看出,列舉類單例模式可以很好的解決序列化問題,



由上面的原始碼分析及查詢資料可知,在序列化的時候Java僅僅是將列舉物件的name屬性輸出到結果中,反序列化的時候則是通過java.lang.Enum的valueOf方法來根據名字查找列舉物件,同時,編譯器是不允許任何對這種序列化機制的定制的,因此禁用了writeObject、readObject等方法,
2.4.2.2反射攻擊測驗
public static void main(String[] args) throws Exception{
Constructor<EnumInstance> c = EnumInstance.class.getDeclaredConstructor(String.class, int.class);
c.setAccessible(true);
EnumInstance instance1 = c.newInstance("mazouri", 1);
}

查看原始碼發現,如果發現列舉型別通過反射構造物件,是會拋出例外的,

2.4.2.3 JAD反編譯
// Decompiled Using: FrontEnd Plus v2.03 and the JAD Engine
// Available From: http://www.reflections.ath.cx
// Decompiler options: packimports(3)
// Source File Name: EnumInstance.java
package com.mazouri.design.pattern.creational.singleton.lazy5;
public final class EnumInstance extends Enum
{
public static EnumInstance[] values()
{
return (EnumInstance[])$VALUES.clone();
}
public static EnumInstance valueOf(String name)
{
return (EnumInstance)Enum.valueOf(com/mazouri/design/pattern/creational/singleton/lazy5/EnumInstance, name);
}
private EnumInstance(String s, int i)
{
super(s, i);
}
public void setData(Object data)
{
this.data = data;
}
public Object getData()
{
return data;
}
public static EnumInstance getInstance()
{
return INSTANCE;
}
public static final EnumInstance INSTANCE;
private Object data;
private static final EnumInstance $VALUES[];
static
{
INSTANCE = new EnumInstance("INSTANCE", 0);
$VALUES = (new EnumInstance[] {
INSTANCE
});
}
}
反編譯結果可知:自己定義的列舉屬性INSTANCE會在前面自動加上 public static final,同時會在靜態代碼塊中進行初始化,而靜態代碼塊在類加載的時候就會被初始化,所以是執行緒安全的,
關于列舉是不是懶加載,我也不清楚,
StaciOverflow上面說是,
總的來說,使用列舉來實作單例模式會非常簡潔,而且提供了序列化的機制,并由JVM從根本上提供保障,絕對防止多次實體化,并且天然執行緒安全,是更簡潔,高效,安全的方式,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/249868.html
標籤:其他
