2020-12-08——單例模式在JDK以及Spring原始碼中如何進行串聯

蒙奇D灬小武發表於2020-12-08

建立型模式

目錄

1、單例模式

1.1 單例模式UML圖

1.2 日常生活中看單例模式

1.3 使用場景

1.4 具體例子

1.4.1 背景

1.4.2 網站計數的單例實現

2、單例模式在原始碼中的應用

2.1 JDK原始碼中單例模式

2.2 Spring原始碼中單例模式

3、單例模式優缺點與場景

3.1 優點

3.2 缺點

3.3 使用注意事項

3.4 適用場景

3.5 應用場景舉例 

4、實現單利模式的原則和過程


1、單例模式

單例模式(Singleton Pattern)是 Java 中最簡單的設計模式之一。許多時候整個系統只需要擁有一個的全域性物件,這樣有利於我們協調系統整體的行為。比如在某個伺服器程式中,該伺服器的配置資訊存放在一個檔案中,這些配置資料由一個單例物件統一讀取,然後服務程式中的其他物件再通過這個單例物件獲取這些配置資訊。這種方式簡化了在複雜環境下的配置管理。

意圖:保證一個類僅有一個例項,並提供一個訪問它的全域性訪問點。

主要解決:一個全域性使用的類頻繁地建立與銷燬。

何時使用:當您想控制例項數目,節省系統資源的時候。

如何解決:判斷系統是否已經有這個單例,如果有則返回,如果沒有則建立。

關鍵程式碼:建構函式是私有的。

1.1 單例模式UML圖

單例模式的 UML 圖

1.2 日常生活中看單例模式

  • 1、一個班級只有一個班主任。
  • 2、Windows 是多程式多執行緒的,在操作一個檔案的時候,就不可避免地出現多個程式或執行緒同時操作一個檔案的現象,所以所有檔案的處理必須通過唯一的例項來進行。
  • 3、一些裝置管理器常常設計為單例模式,比如一個電腦有兩臺印表機,在輸出的時候就要處理不能兩臺印表機列印同一個檔案。

1.3 使用場景

  • 1、要求生產唯一序列號。
  • 2、WEB 中的計數器,不用每次重新整理都在資料庫里加一次,用單例先快取起來。
  • 3、建立的一個物件需要消耗的資源過多,比如 I/O 與資料庫的連線等。

注意事項:getInstance() 方法中需要使用同步鎖 synchronized (Singleton.class) 防止多執行緒同時進入造成 instance 被多次例項化。

1.4 具體例子

1.4.1 背景

  • 在企業網站後臺系統中,一般會將網站統計單元進行獨立設計,比如登入人數的統計、IP數量的計數等。在這類需要完成全域性統計的過程中,就會用到單例模式即整個系統只需要擁有一個計數的全域性物件。
  • 在網站登入這個高併發場景下,由這個全域性物件負責統計當前網站的登入人數、IP等,即節約了網站伺服器的資源,又能保證計數的準確性。

在這裡插入圖片描述

1.4.2 網站計數的單例實現

實現單例模式有多種寫法,這裡我們只列舉其中最常用的三種實現方式,且考慮到網站登入高併發場景下,將重點關注多執行緒環境下的安全問題。

在這裡插入圖片描述

登入執行緒的實現:我們先建立一個登入執行緒類,用於登入及登入成功後呼叫單例物件進行計數。

/**
 * 單例模式的應用--登入執行緒
 *
 * @author zhuhuix
 * @date 2020-06-01
 */
public class Login implements Runnable {
	// 登入名稱
    private String loginName;

    public String getLoginName() {
        return loginName;
    }

    public void setLoginName(String loginName) {
        this.loginName = loginName;
    }

    @Override
    public void run() {
		// TODO 
		// 登入成功後呼叫單例物件進行計數
    }
}

主程式的實現:編寫一個主程式,利用多執行緒技術模擬10個使用者併發登入,完成登入後輸出登入人次計數。

/**
 * 單例模式--主程式
 *
 * @author zhuhuix
 * @date 2020-06-01
 */
public class App {
    public final static int num = 10;

    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[num];

        for (int i = 0; i < num; i++) {
            Login login = new Login();
            login.setLoginName("" + String.format("%2s", (i + 1)) + "號使用者");
            threads[i] = new Thread(login);
            threads[i].start();
        }

        for (int i = 0; i < threads.length; i++) {
            threads[i].join();
        }

		// TODO
		// 呼叫單例物件輸出登入人數統計
}

1.4.2.1 餓漢模式

  • 在程式啟動之初就進行建立( 不管三七二十一,先建立出來再說)。
  • 天生的執行緒安全。
  • 無論程式中是否用到該單例類都會存在。
/**
 * 餓漢式單例模式
 *
 * @author zhuhuix
 * @date 2020-06-01
 */
public class SimpleSingleton implements Serializable {
    // 單例物件
    private static final SimpleSingleton APP_INSTANCE = new SimpleSingleton();
    // 計數器
    private AtomicLong count = new AtomicLong(0);

    // 單例模式必須保證預設構造方法為私有型別
    private SimpleSingleton() {
    }

    public static SimpleSingleton getInstance() {
        return APP_INSTANCE;
    }

    public AtomicLong getCount() {
        return count;
    }

    public void setCount() {
        count.addAndGet(1);
    }

}

我們將餓漢模式的單例物件加入進登入執行緒及主程式中進行測試:

/**
 * 單例模式的應用--登入執行緒
 *
 * @author zhuhuix
 * @date 2020-06-01
 */
public class Login implements Runnable {
    // 登入名稱
    private String loginName;

    public String getLoginName() {
        return loginName;
    }

    public void setLoginName(String loginName) {
        this.loginName = loginName;
    }

    @Override
    public void run() {
    	// 餓漢式單例
        SimpleSingleton simpleSingleton=  SimpleSingleton.getInstance();
        simpleSingleton.setCount();
        System.out.println(getLoginName()+"登入成功:"+simpleSingleton.toString());
    }

}

/**
 * 單例模式--主程式
 *
 * @author zhuhuix
 * @date 2020-06-01
 */
public class App {
    public final static int num = 10;
    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[num];
        for (int i = 0; i < num; i++) {
            Login login = new Login();
            login.setLoginName("" + String.format("%2s", (i + 1)) + "號使用者");
            threads[i] = new Thread(login);
            threads[i].start();
        }
        for (int i = 0; i < threads.length; i++) {
            threads[i].join();
        }
        System.out.println("網站共有"+SimpleSingleton.getInstance().getCount()+"個使用者登入");

    }
}

輸出如下:
10個執行緒併發登入過程中,獲取到了同一個物件引用地址,即該單例模式是有效的。

在這裡插入圖片描述

1.4.2.2 懶漢模式

  • 在初始化時只進行定義。
  • 只有在程式中呼叫了該單例類,才會完成例項化( 沒人動我,我才懶得動)。
  • 需通過執行緒同步技術才能保證執行緒安全。

我們先看下未使用執行緒同步技術的例子:

/**
 * 懶漢式單例模式--未應用執行緒同步技術
 *
 * @author zhuhuix
 * @date 2020-06-01
 */
public class LazySingleton {
    // 單例物件
    private static LazySingleton APP_INSTANCE;
    // 計數器
    private AtomicLong count = new AtomicLong(0);

    // 單例模式必須保證預設構造方法為私有型別
    private LazySingleton() {
    }

    public static LazySingleton getInstance() {
        if (APP_INSTANCE == null) {
            APP_INSTANCE = new LazySingleton();
        }
        return APP_INSTANCE;
    }

    public AtomicLong getCount() {
        return count;
    }

    public void setCount() {
        count.addAndGet(1);
    }

  }
/**
 * 單例模式的應用--登入執行緒
 *
 * @author zhuhuix
 * @date 2020-06-01
 */
public class Login implements Runnable {
   
	....
    @Override
    public void run() {
		// 餓漢式單例
        LazySingleton lazySingleton =LazySingleton.getInstance();
        lazySingleton.setCount();
        System.out.println(getLoginName()+"登入成功:"+lazySingleton);
    }

}

/**
 * 單例模式--主程式-
 *
 * @author zhuhuix
 * @date 2020-06-01
 */
public class App {
    public final static int num = 10;
    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[num];
        for (int i = 0; i < num; i++) {
            Login login = new Login();
            login.setLoginName("" + String.format("%2s", (i + 1)) + "號使用者");
            threads[i] = new Thread(login);
            threads[i].start();
        }
        for (int i = 0; i < threads.length; i++) {
            threads[i].join();
        }
        System.out.println("網站共有" + LazySingleton.getInstance().getCount() + "個使用者登入");
    }
}

輸出結果:
10個執行緒併發登入過程中,獲取到了四個物件引用地址,該單例模式失效了。

在這裡插入圖片描述

對程式碼進行分析:

// 未使用執行緒同步
public static LazySingleton getInstance() {
		// 在多個執行緒併發時,可能會有多個執行緒同時進入 if 語句,導致產生多個例項
        if (APP_INSTANCE == null) {
            APP_INSTANCE = new LazySingleton();
        }
        return APP_INSTANCE;
    }

我們使用執行緒同步技術對懶漢式模式進行改進:

/**
 * 懶漢式單例模式
 *
 * @author zhuhuix
 * @date 2020-06-01
 */
public class LazySingleton {
    // 單例物件 ,加入volatile關鍵字進行修飾
    private static volatile LazySingleton APP_INSTANCE;
    // 計數器
    private AtomicLong count = new AtomicLong(0);

    // 單例模式必須保證預設構造方法為私有型別
    private LazySingleton() {
    }

    public static LazySingleton getInstance() {
        if (APP_INSTANCE == null) {
            // 對類進行加鎖,並進行雙重檢查
            synchronized (LazySingleton.class) {
                if (APP_INSTANCE == null) {
                    APP_INSTANCE = new LazySingleton();
                }
            }
        }
        return APP_INSTANCE;
    }

    public AtomicLong getCount() {
        return count;
    }

    public void setCount() {
        count.addAndGet(1);
    }

  }

再測試執行:
10個執行緒併發登入過程中,獲取到了同一個物件引用地址,即該單例模式有效了。

在這裡插入圖片描述

1.4.2.3 列舉類實現單例模式

《Effective Java》 推薦使用列舉的方式解決單例模式。這種方式解決了最主要的;執行緒安全、自由序列化、單一例項。

/**
 * 利用列舉類實現單例模式
 *
 * @author zhuhuix
 * @date 2020-06-01
 */
public enum EnumSingleton implements Serializable {
    // 單例物件
    APP_INSTANCE;
    // 計數器
    private AtomicLong count = new AtomicLong(0);

    // 單例模式必須保證預設構造方法為私有型別
    private EnumSingleton() {
    }

    public AtomicLong getCount() {
        return count;
    }

    public void setCount() {
        count.addAndGet(1);
    }
    
}
/**
 * 單例模式的應用--登入執行緒
 *
 * @author zhuhuix
 * @date 2020-06-01
 */
public class Login implements Runnable {
    ...
    @Override
    public void run() {
         EnumSingleton enumSingleton = EnumSingleton.APP_INSTANCE;
         enumSingleton.setCount();
        System.out.println(getLoginName()+"登入成功:"+enumSingleton.toString());

    }
}

/**
 * 單例模式--主程式
 *
 * @author zhuhuix
 * @date 2020-06-01
 */
public class App {
    public final static int num = 10;
    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[num];
        for (int i = 0; i < num; i++) {
            Login login = new Login();
            login.setLoginName("" + String.format("%2s", (i + 1)) + "號使用者");
            threads[i] = new Thread(login);
            threads[i].start();
        }
        for (int i = 0; i < threads.length; i++) {
            threads[i].join();
        }
         System.out.println("網站共有"+EnumSingleton.APP_INSTANCE.getCount()+"個使用者登入");

    }
}

輸出如下:
10個執行緒併發登入過程中,該單例模式是有效的。

在這裡插入圖片描述

2、單例模式在原始碼中的應用

2.1 JDK原始碼中單例模式

java.lang.Runtime使用了單例模式的餓漢式,原始碼如下:

在這裡插入圖片描述

2.2 Spring原始碼中單例模式

在 Spring 依賴注入Bean例項預設是單例的,我們由此展開。bean 可以被定義為兩種模式:prototype(多例)和 singleton(單例)。

  • singleton(單例):只有一個共享的例項存在,所有對這個 bean 的請求都會返回唯一的例項。
  • prototype(多例):對這個 bean 的每次請求都會建立一個新的 bean 例項,類似於 new。

例項:配置檔案內容如下:

<bean id="singleton" class="java.util.Date" scope="singleton"></bean>
<bean id="prototype" class="java.util.Date" scope="prototype"></bean>

Spring 中載入單例的過程都是在 BeanFactory 的 getBean() 方法中被定義的,其預設的功能在 AbstractBeanFactory 類中實現,主要包含兩個功能。

  1. 從快取中獲取單例 Bean。
  2. Bean 的例項中獲取物件。

getBean() 方法最終會呼叫 AbstractBeanFactory 的 doGetBean() 方法,原始碼如下。

protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType,
                          @Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {
    //對傳入的beanName稍作修改,防止有一些非法欄位,然後提取Bean的Name
    final String beanName = transformedBeanName(name);
    Object bean;
    //直接從快取中獲取單例工廠中的objectFactory單例
    Object sharedInstance = getsingleton(beanName);
    if (sharedInstance != null && args == null) {
        if (logger.isDebugEnabled()) {
            if (isSingletonCurrentlyInCreation(beanName)) {
                logger.debug("Returning eagerly cached instance of singleton bean '" +
                        beanName + "' that is not fully initialized yet - a consequence of a circular reference");
            } else {
            }
        }
        //返回對應的例項,從 Bean例項中獲取物件
        bean = getObjectForBeanInstance(sharedInstance,name,beanName, null);
    } else {
        ...
    }
    ...
}

getBean() 方法不僅處理單例物件的邏輯,還處理原型物件的邏輯。繼續看 getSingleton() 方法的程式碼實現。
getSingleton() 的工作流程:singletonObjects-->earlySingletonObjects-->singletonFactories-->建立單例例項

/**
* 單例物件的快取
*/
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    //首先通過名字查詢這個Bean是否存在
    Object singletonObject = this.singletonObjects.get(beanName);
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        synchronized (this.singletonObjects) {
            //檢視快取中是否存在這個Bean
            singletonObject = this.earlySingletonObjects.get(beanName);
            //如果這個時候的Bean例項還為空並且允許懶載入
            if (singletonObjects == null && allowEarlyReference) {
                ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                if (singletonFactory != null) {
                    singletonObject = singletonFactory.getObject();
                    this.earlySingletonObjects.put(beanName, singletonObject);
                    this.singletonFactories.remove(beanName);
                }
            }
        }
    }
    return singletonObject;
}

在上面程式碼片段中,synchronized(this.singletonObjects) 是關鍵,但是前提條件 isSingletonCurrentlyInCreation 的返回值也是 true,也就是這個 Bean 正在被建立。因此,第一次呼叫 doGetBean() 的時候,getSingleton() 基本上都是返回 null,所以會繼續執行 doGetBean() 方法中後面的邏輯。

protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType,
            @Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {
    // 獲取beanDefinition
    final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
                checkMergedBeanDefinition(mbd, beanName, args);
                // Guarantee initialization of beans that the current bean depends on.
                String[] dependsOn = mbd.getDependsOn();
                if (dependsOn != null) {
                    for (String dep : dependsOn) {
                        if (isDependent(beanName, dep)) {
                            throw new BeanCreationException(mbd.getResourceDescription(), beanName,
                                    "Circular depends-on relationship between '" + beanName + "' and '" + dep + "'");
                        }
                        registerDependentBean(dep, beanName);
                        try {
                            getBean(dep);
                        }
                        catch (NoSuchBeanDefinitionException ex) {
                            throw new BeanCreationException(mbd.getResourceDescription(), beanName,
                                    "'" + beanName + "' depends on missing bean '" + dep + "'", ex);
                        }
                    }
                }
                // Create bean instance.
                if (mbd.isSingleton()) {
                    sharedInstance = getSingleton(beanName, () -> {
                        try {
                            return createBean(beanName, mbd, args);
                        }
                        catch (BeansException ex) {
                            // Explicitly remove instance from singleton cache: It might have been put there
                            // eagerly by the creation process, to allow for circular reference resolution.
                            // Also remove any beans that received a temporary reference to the bean.
                            destroySingleton(beanName);
                            throw ex;
                        }
                    });
                    bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
                }
            }
            ...
        }
}

可以看到,在 BeanFactory 中,從 XML 中解析出來的相關配置資訊被放在 BeanDefinitionMap 中,通過這個 Map 獲取 RootBeanDefinition,然後執行判斷語句if(mbd.isSingleton())。如果是單例的,則接著呼叫 getSingleton() 的過載方法,傳入 mbd 引數。當從快取中載入單例物件時,會把當前的單例物件在singletonObjects 中存放一份,這樣可以保證在呼叫 getBean() 方法的時候,singletonObjects 中永遠只有一個例項,在獲取物件時才會給它分配記憶體,既保證了記憶體高效利用,又是執行緒安全的。

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
    Assert.notNull(beanName, "Bean name must not be null");
    synchronized (this.singletonObjects) {
    // 直接從快取中獲取單例Bean
        Object singletonObject = this.singletonObjects.get(beanName);
        if (singletonObject == null) {
            if (this.singletonsCurrentlyInDestruction) {
                throw new BeanCreationNotAllowedException(beanName,
                        "Singleton bean creation not allowed while singletons of this factory are in destruction " +
                        "(Do not request a bean from a BeanFactory in a destroy method implementation!)");
            }
            if (logger.isDebugEnabled()) {
                logger.debug("Creating shared instance of singleton bean '" + beanName + "'");
            }
            beforeSingletonCreation(beanName);
            boolean newSingleton = false;
            boolean recordSuppressedExceptions = (this.suppressedExceptions == null);
            if (recordSuppressedExceptions) {
                this.suppressedExceptions = new LinkedHashSet<>();
            }
            try {
                singletonObject = singletonFactory.getObject();
                newSingleton = true;
            }
            catch (IllegalStateException ex) {
                // Has the singleton object implicitly appeared in the meantime ->
                // if yes, proceed with it since the exception indicates that state.
                singletonObject = this.singletonObjects.get(beanName);
                if (singletonObject == null) {
                    throw ex;
                }
            }
            catch (BeanCreationException ex) {
                if (recordSuppressedExceptions) {
                    for (Exception suppressedException : this.suppressedExceptions) {
                        ex.addRelatedCause(suppressedException);
                    }
                }
                throw ex;
            }
            finally {
                if (recordSuppressedExceptions) {
                    this.suppressedExceptions = null;
                }
                afterSingletonCreation(beanName);
            }
            if (newSingleton) {
                // 在singletonObject中新增要載入的單例
                addSingleton(beanName, singletonObject);
            }
        }
        return singletonObject;
    }
}

如此一來,當下次需要這個單例 Bean 時,可以直接從快取中獲取。在 Spring 中建立單例的過程雖然有點繞,但是邏輯非常清楚,就是將需要的物件放在 Map 中,下次需要的時候直接從 Map 中獲取即可。

3、單例模式優缺點與場景

3.1 優點

 

  1.   在單例模式中,活動的單例只有一個例項,對單例類的所有例項化得到的都是相同的一個例項。這樣就 防止其它物件對自己的例項化,確保所有的物件都訪問一個例項 
  2.   單例模式具有一定的伸縮性,類自己來控制例項化程式,類就在改變例項化程式上有相應的伸縮性。 
  3.   提供了對唯一例項的受控訪問。 
  4.   由於在系統記憶體中只存在一個物件,因此可以 節約系統資源,當 需要頻繁建立和銷燬的物件時單例模式無疑可以提高系統的效能。 
  5.   允許可變數目的例項。 
  6.   避免對共享資源的多重佔用。 

3.2 缺點

  1.     不適用於變化的物件,如果同一型別的物件總是要在不同的用例場景發生變化,單例就會引起資料的錯誤,不能儲存彼此的狀態。 
  2.     由於單利模式中沒有抽象層,因此單例類的擴充套件有很大的困難。 
  3.     單例類的職責過重,在一定程度上違背了“單一職責原則”。 
  4.     濫用單例將帶來一些負面問題,如為了節省資源將資料庫連線池物件設計為的單例類,可能會導致共享連線池物件的程式過多而出現連線池溢位;如果例項化的物件長時間不被利用,系統會認為是垃圾而被回收,這將導致物件狀態的丟失。 

3.3 使用注意事項

  •     1.使用時不能用反射模式建立單例,否則會例項化一個新的物件 
  •     2.使用懶單例模式時注意執行緒安全問題 
  •     3.餓單例模式和懶單例模式構造方法都是私有的,因而是不能被繼承的,有些單例模式可以被繼承(如登記式模式) 

3.4 適用場景

單例模式只允許建立一個物件,因此節省記憶體,加快物件訪問速度,因此物件需要被公用的場合適合使用,如多個模組使用同一個資料來源連線物件等等。如: 

  •     1.需要頻繁例項化然後銷燬的物件。 
  •     2.建立物件時耗時過多或者耗資源過多,但又經常用到的物件。 
  •     3.有狀態的工具類物件。 
  •     4.頻繁訪問資料庫或檔案的物件。 

以下都是單例模式的經典使用場景: 

  •     1.資源共享的情況下,避免由於資源操作時導致的效能或損耗等。如上述中的日誌檔案,應用配置。 
  •     2.控制資源的情況下,方便資源之間的互相通訊。如執行緒池等。 

3.5 應用場景舉例 

  •     1.外部資源:每臺計算機有若干個印表機,但只能有一個PrinterSpooler,以避免兩個列印作業同時輸出到印表機。內部資源:大多數軟體都有一個(或多個)屬性檔案存放系統配置,這樣的系統應該有一個物件管理這些屬性檔案 
  •     2. Windows的Task Manager(工作管理員)就是很典型的單例模式(這個很熟悉吧),想想看,是不是呢,你能開啟兩個windows task manager嗎? 不信你自己試試看哦~ 
  •     3. windows的Recycle Bin(回收站)也是典型的單例應用。在整個系統執行過程中,回收站一直維護著僅有的一個例項。 
  •     4. 網站的計數器,一般也是採用單例模式實現,否則難以同步。 
  •     5. 應用程式的日誌應用,一般都何用單例模式實現,這一般是由於共享的日誌檔案一直處於開啟狀態,因為只能有一個例項去操作,否則內容不好追加。 
  •     6. Web應用的配置物件的讀取,一般也應用單例模式,這個是由於配置檔案是共享的資源。 
  •     7. 資料庫連線池的設計一般也是採用單例模式,因為資料庫連線是一種資料庫資源。資料庫軟體系統中使用資料庫連線池,主要是節省開啟或者關閉資料庫連線所引起的效率損耗,這種效率上的損耗還是非常昂貴的,因為何用單例模式來維護,就可以大大降低這種損耗。 
  •     8. 多執行緒的執行緒池的設計一般也是採用單例模式,這是由於執行緒池要方便對池中的執行緒進行控制。 
  •     9. 作業系統的檔案系統,也是大的單例模式實現的具體例子,一個作業系統只能有一個檔案系統。 
  •     10. HttpApplication 也是單位例的典型應用。熟悉ASP.Net(IIS)的整個請求生命週期的人應該知道HttpApplication也是單例模式,所有的HttpModule都共享一個HttpApplication例項. 

4、實現單利模式的原則和過程

  •     1.單例模式:確保一個類只有一個例項,自行例項化並向系統提供這個例項 
  •     2.單例模式分類:餓單例模式(類載入時例項化一個物件給自己的引用),懶單例模式(呼叫取得例項的方法如getInstance時才會例項化物件)(java中餓單例模式效能優於懶單例模式,c++中一般使用懶單例模式) 
  •     3.單例模式要素: 
  •         a.私有構造方法 
  •         b.私有靜態引用指向自己例項 
  •         c.以自己例項為返回值的公有靜態方法 

參考文章:

https://www.cnblogs.com/zhuhuix/p/13030646.html

http://m.biancheng.net/view/8378.html

https://www.cnblogs.com/damsoft/p/6105122.html

相關文章