概述
?單例模式(Singleton Pattern)是確保一個(gè)類在任何情況下都絕對(duì)只有一個(gè)實(shí)例,并提供一個(gè)全局訪問點(diǎn)。單例模式是創(chuàng)建型模式。J2EE的標(biāo)準(zhǔn)的ServletContext、ServletContextConfig等、Spring框架應(yīng)用中的ApplicationContext、數(shù)據(jù)庫連接池等也都是單例模式。
1.餓漢式單例模式
先來看看類結(jié)構(gòu)圖:

?餓漢式單例模式在類加載的時(shí)候就立刻初始化,并且創(chuàng)建單例對(duì)象。他絕對(duì)線程安全,在線程出現(xiàn)之前就已經(jīng)實(shí)例化,不存在安全訪問問題。
優(yōu)點(diǎn):沒有添加任何鎖、執(zhí)行效率高,用戶體驗(yàn)比懶漢式單例模式更好。
缺點(diǎn):類加載時(shí)就初始化,會(huì)造成內(nèi)存浪費(fèi)。(內(nèi)存換效率)
來看下代碼:
/**
* @Author: zhouzhen
* @email: zhouzhen0517@foxmail.com
* @Description 提供一個(gè)全局訪問點(diǎn),并隱藏所有的構(gòu)造方法
* 餓漢式單例 優(yōu)點(diǎn):執(zhí)行效率高,沒有任何鎖
* 缺點(diǎn): 會(huì)造成內(nèi)存浪費(fèi)
* @Date: Create in 10:34 2020/4/12
*/
public class HungrySingleton {
private static final HungrySingleton hungrySingleton = new HungrySingleton();
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return hungrySingleton;
}
}
靜態(tài)代碼塊的實(shí)現(xiàn)方式:
/**
* @Author: zhouzhen
* @email: zhouzhen0517@foxmail.com
* @Description
* @Date: Create in 10:46 2020/4/12
*/
public class HungryStaticSingleton {
private static final HungryStaticSingleton hungryStaticSingleton;
static {
hungryStaticSingleton = new HungryStaticSingleton();
}
private HungryStaticSingleton() {
}
public static HungryStaticSingleton getInstance() {
return hungryStaticSingleton;
}
}
?兩種寫法并沒有本質(zhì)上的區(qū)別,餓漢式單例模式適用于單例對(duì)象較少的情況。下面我們來看性能更優(yōu)的寫法。
2.懶漢式單例模式
?懶漢式單例模式的特點(diǎn)是:只有被外部類調(diào)用的時(shí)候才會(huì)生成對(duì)應(yīng)單例對(duì)象的實(shí)例。下面看懶漢式單例模式的簡單實(shí)現(xiàn)LazySimpleSingleton:
/**
* @Author: zhouzhen
* @email: zhouzhen0517@foxmail.com
* @Description
* @Date: Create in 11:17 2020/4/12
*/
public class LazySimpleSingleton {
private static LazySimpleSingleton instance;
private LazySimpleSingleton() {
}
public static LazySimpleSingleton getInstance() {
if(instance == null) {
//打印當(dāng)前創(chuàng)建的實(shí)例
System.out.println(Thread.currentThread().getName() + ":"
+ (instance = new LazySimpleSingleton()));
}
return instance;
}
}
然后寫一個(gè)線程類ExectorThread:
/**
* @Author: zhouzhen
* @email: zhouzhen0517@foxmail.com
* @Description
* @Date: Create in 22:37 2020/4/12
*/
public class ExectorThread implements Runnable {
public void run() {
LazySimpleSingleton.getInstance();
}
}
調(diào)用代碼:
/**
* @Author: zhouzhen
* @email: zhouzhen0517@foxmail.com
* @Description
* @Date: Create in 22:40 2020/4/12
*/
public class LazySimpleSingletonTest {
public static void main(String[] args) {
Thread t1 = new Thread(new ExectorThread());
Thread t2 = new Thread(new ExectorThread());
t1.start();
t2.start();
}
}
運(yùn)行結(jié)果:

?可以發(fā)現(xiàn),在并發(fā)情況下,上述代碼有一定概率會(huì)創(chuàng)建兩個(gè)不同的對(duì)象,破壞了實(shí)例對(duì)象的全局單例,存在線程安全隱患。下面我們用synchronized關(guān)鍵字來保證getInstance()的線程安全。
/**
* @Author: zhouzhen
* @email: zhouzhen0517@foxmail.com
* @Description
* @Date: Create in 11:17 2020/4/12
*/
public class LazySimpleSingleton {
private static LazySimpleSingleton instance;
private LazySimpleSingleton() {
}
public synchronized static LazySimpleSingleton getInstance() {
if(instance == null) {
System.out.println(Thread.currentThread().getName() + ":"
+ (instance = new LazySimpleSingleton()));
}
return instance;
}
}
?通過上述方法,線程安全問題解決了,但在線程數(shù)量比較多的情況下,CPU分配壓力上升,則會(huì)導(dǎo)致大批線程阻塞,導(dǎo)致程序性能大幅下降。如何解決這個(gè)問題呢,來看看雙重檢查鎖的單例模式:
/**
* @Author: zhouzhen
* @email: zhouzhen0517@foxmail.com
* @Description
* @Date: Create in 11:17 2020/4/12
*/
public class LazySimpleSingleton {
private volatile static LazySimpleSingleton instance;
private LazySimpleSingleton() {
}
public synchronized static LazySimpleSingleton getInstance() {
if(instance == null) {
synchronized(LazySimpleSingleton.class) {
if(instance == null) {
System.out.println(Thread.currentThread().getName() + ":"
+ (instance = new LazySimpleSingleton()));
}
}
}
return instance;
}
?當(dāng)?shù)谝粋€(gè)線程調(diào)用getInstance()方法時(shí),第二個(gè)線程也可以調(diào)用。當(dāng)?shù)谝粋€(gè)線程執(zhí)行到synchronized時(shí)會(huì)上鎖,第二個(gè)線程就會(huì)變成阻塞狀態(tài)。此時(shí)的阻塞不同于之前整個(gè)類的阻塞,而是在getInstance()方法內(nèi)部的阻塞,對(duì)于調(diào)用者的性能影響感知不強(qiáng)。
?在instance前添加volatile 關(guān)鍵字是為了解決指令重排序問題,可以參考https://blog.csdn.net/hl_java/article/details/89160086
?當(dāng)然,用到synchronized關(guān)鍵字總歸要上鎖,對(duì)程序性能還是存在影響的。來看從類的初始化角度考慮的,采用靜態(tài)內(nèi)部類的方法實(shí)現(xiàn)的終極解決方案:
/**
* @Author: zhouzhen
* @email: zhouzhen0517@foxmail.com
* @Description
* @Date: Create in 23:08 2020/4/12
*/
public class LazyInnerClassSingleton {
//使用LazyInnerClassGeneral的時(shí)候,默認(rèn)會(huì)先初始化內(nèi)部類
//如果沒有被使用,則內(nèi)部類是不被加載的
private LazyInnerClassSingleton() {
}
//static保證了單例的空間共享,final保證這個(gè)方法不會(huì)被重寫和重載
public static final LazyInnerClassSingleton getInstance() {
//在返回結(jié)果之前,一定會(huì)先加載內(nèi)部類
return LazyHolder.LAZY;
}
//默認(rèn)不加載
private static class LazyHolder {
private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
}
}
?靜態(tài)內(nèi)部類在ClassPath下的形式是LazyInnerClassSingleton$LazyHolder.class的形式,LazyInnerClassSingleton類加載時(shí)不會(huì)第一時(shí)間掃描內(nèi)部類,只有在getInstance()方法被調(diào)用時(shí),內(nèi)部類才會(huì)初始化,然后生成實(shí)例。這種方式兼顧了懶漢式單例模式的內(nèi)存浪費(fèi)問題和synchronized的性能問題,巧妙的避免了線程安全問題。
文章參考
《Spring5核心原理》〔中〕譚勇德(Tom)