之前我們在學習javase的時候曾學習了單例模式,單例模式應該是設計模式中比較簡單的一個,也是非常常見的,但是在多線程併發的環境下使用卻是不那麼簡單了,今天給大家分享一個我在開發過程中遇到的單例模式的應用。
單例模式的定義:
一個類有且僅有一個實例,並且自行實例化向整個系統提供。
單例模式的要素:
- 1.私有的靜態的實例對象
- 2.私有的構造函數(保證在該類外部,無法通過new的方式來創建對象實例)
- 3.公有的、靜態的、訪問該實例對象的方法
單例模式分爲懶漢形和餓漢式
懶漢式 :
應用剛啓動的時候,並不創建實例,當外部調用該類的實例或者該類實例方法的時候,才創建該類的實例。(時間換空間)
優點:實例在被使用的時候才被創建,可以節省系統資源,體現了延遲加載的思想。
缺點:由於系統剛啓動時且未被外部調用時,實例沒有創建;如果一時間有多個線程同時調用LazySingleton.getLazyInstance()方法很有可能會產生多個實例。
例子:
public class SingletonClass{
//私有構造函數,保證類不能通過new創建
private SingletonClass(){}
private static SingletonClass instance=null;
public static SingletonClass getInstance(){
if(instance==null){
//創建本類對象
instance=new SingletonClass();
}
return instance;
}
}
餓漢式:
應用剛啓動的時候,不管外部有沒有調用該類的實例方法,該類的實例就已經創建好了。(空間換時間。)
優點:寫法簡單,在多線程下也能保證單例實例的唯一性,不用同步,運行效率高。
缺點:在外部沒有使用到該類的時候,該類的實例就創建了,若該類實例的創建比較消耗系統資源,並且外部一直沒有調用該實例,那麼這部分的系統資源的消耗是沒有意義的。
例子:
public class Singleton{
//首先自己在內部定義自己的一個實例,只供內部調用
private static final Singleton instance = new Singleton();
//私有構造函數
private Singleton(){
}
//提供了靜態方法,外部可以直接調用
public static Singleton getInstance(){
return instance;
}
}
但是單例模式也不是一定安全的,比如在多線程下會出現問題,下面模擬效果
public class LazySingleton {
//爲了易於模擬多線程下,懶漢式出現的問題,我們在創建實例的構造函數裏面使當前線程暫停了50毫秒
private LazySingleton() {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("生成LazySingleton實例一次!");
}
private static LazySingleton lazyInstance = null;
public static LazySingleton getLazyInstance() {
if (lazyInstance == null) {
lazyInstance = new LazySingleton();
}
return lazyInstance;
}
}
public class SingletonTest {
public static void main(String[] args) {
//創建十個線程調
for (int i = 0; i < 10; i++) {
new Thread(){
@Override
public void run() {
LazySingleton.getLazyInstance();
}
}.start();
}
}
}
結果:
生成LazySingleton實例一次!
生成LazySingleton實例一次!
生成LazySingleton實例一次!
生成LazySingleton實例一次!
生成LazySingleton實例一次!
生成LazySingleton實例一次!
生成LazySingleton實例一次!
生成LazySingleton實例一次!
生成LazySingleton實例一次!
生成LazySingleton實例一次!
可以看出單例模式懶漢式在多線程的併發下也會出現問題.
分析一下:多個線程同時訪問上面的懶漢式單例,現在有兩個線程A和B同時訪問LazySingleton.getLazyInstance()方法。
假設A先得到CPU的時間切片,A執行到 if (lazyInstance == null) 時,由於lazyInstance 之前並沒有實例化,所以lazyInstance == null爲true,在還沒有執行實例創建的時候
此時CPU將執行時間分給了線程B,線程B執行到 if (lazyInstance == null) 時,由於lazyInstance 之前並沒有實例化,所以lazyInstance == null爲true,線程B繼續往下執行實例的創建過程,線程B創建完實例之後,返回。
此時CPU將時間切片分給線程A,線程A接着開始執行實例的創建,實例創建完之後便返回。由此看線程A和線程B分別創建了一個實例(存在2個實例了),這就導致了單例的失效。解決辦法:我們可以在getLazyInstance方法上加上synchronized使其同步,但是這樣一來,會降低整個訪問的速度,而且每次都要判斷。那麼有沒有更好的方式來實現呢?我們可以考慮使用”雙重檢查加鎖”的方式來實現,就可以既實現線程安全,又能夠使性能不受到很大的影響。我們看看具體解決代碼
public class LazySingleton {
private LazySingleton() {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("生成LazySingleton實例一次!");
}
private static LazySingleton lazyInstance = null;
public static LazySingleton getLazyInstance() {
//先檢查實例是否存在,如果不存在才進入下面的同步塊
if (lazyInstance == null) {
//同步塊,線程安全地創建實例
synchronized(LazySingleton.class){
//再次檢查實例是否存在,如果不存在才真正地創建實例
if(lazyInstance == null){
lazyInstance = new LazySingleton();
}
}
}
return lazyInstance;
}
}
這樣我們就可以在多線程併發下安全應用單例模式中的懶漢模式。這種方法在代碼上可能就不怎麼美觀,我們可以優雅的使用一個內部類來維護單例類的實例,下面看看代碼
public class GracefulSingleton {
private GracefulSingleton(){
System.out.println("創建GracefulSingleton實例一次!");
}
//類級的內部類,也就是靜態的成員式內部類,該內部類的實例與外部類的實例沒有綁定關係,而且只有被調用到纔會裝載,從而實現了延遲加載
private static class SingletonHoder{
//靜態初始化器,由JVM來保證線程安全
private static GracefulSingleton instance = new GracefulSingleton();
}
public static GracefulSingleton getInstance(){
return SingletonHoder.instance;
}
}
實際項目中的運用
場景:爲了程序的高效率使用多線程併發,然而是循環調用,可能導致創建線程數過多,考慮採用線程池管理,這時候創建線程池仍然是處於循環調用中,也可能導致多個線程池,這時候就考慮使用單例模式。
源代碼:
public class ThreadPoolFactoryUtil {
private ExecutorService executorService;
//在構造函數中創建線程池
private ThreadPoolFactoryUtil(){
//獲取系統處理器個數,作爲線程池數量
int nThreads=Runtime.getRuntime().availableProcessors();
executorService=Executors.newFixedThreadPool(nThreads);
}
//定義一個靜態內部類,內部定義靜態成員創建外部類實例
private static class SingletonContainer{
private static ThreadPoolFactoryUtil util=new ThreadPoolFactoryUtil();
}
//獲取本類對象
public static ThreadPoolFactoryUtil getUtil(){
return SingletonContainer.util;
}
public ExecutorService getExecutorService(){
return executorService;
}
}
涉及到一個靜態內部類,我們看看靜態內部類的特點:
- 1、靜態內部類無需依賴於外部類,它可以獨立於外部對象而存在。
- 2、靜態內部類,多個外部類的對象可以共享同一個內部類的對象。
- 3、使用靜態內部類的好處是加強了代碼的封裝性以及提高了代碼的可讀性。
- 4、普通內部類不能聲明static的方法和變量,注意這裏說的是變量,常量(也就是final static修飾的屬性)還是可以的,而 靜態內部類形似外部類,沒有任何限制。可以直接被用 外部類名+內部類名 獲得。
以上是我在實際開發中遇到的一些問題,部分摘自網上代碼,結合實開發際案例。如有不妥,希望大家及時指出!