前言
在面試的時候面試官會怎麼在單例模式中提問呢?你又該如何回答呢?可能你在面試的時候你會碰到這些問題:
那我們該怎麼回答呢?那答案來了,看完接下來的內容就可以跟面試官嘮嘮單例模式了
單例模式簡介
單例模式是一種常用的軟件設計模式,其屬於創建型模式,其含義即是一個類只有一個實例,併為整個系統提供一個全局訪問點 (向整個系統提供這個實)。
結構:
單例模式三要素:
-
私有的構造方法;
-
私有靜態實例引用;
-
返回靜態實例的靜態公有方法。
單例模式的優點
-
在內存中只有一個對象,節省內存空間;
-
避免頻繁的創建銷毀對象,可以提高性能;
-
避免對共享資源的多重佔用,簡化訪問;
-
為整個系統提供一個全局訪問點。
單例模式的注意事項
在使用單例模式時,我們必須使用單例類提供的公有工廠方法得到單例對象,而不應該使用反射來創建,使用反射將會破壞單例模式 ,將會實例化一個新對象。
單線程實現方式
在單線程環境下,單例模式根據實例化對象時機的不同分為,
從速度和反應時間角度來講,餓漢式(又稱立即加載)要好一些;從資源利用效率上說,懶漢式(又稱延遲加載)要好一些。
餓漢式單例
// 餓漢式單例
public class HungrySingleton{
// 私有靜態實例引用,創建私有靜態實例,並將引用所指向的實例
private static HungrySingleton singleton = new HungrySingleton();
// 私有的構造方法
private HungrySingleton(){}
//返回靜態實例的靜態公有方法,靜態工廠方法
public static HungrySingleton getSingleton(){
return singleton;
}
}
餓漢式單例,在類被加載時,就會實例化一個對象並將引用所指向的這個實例;更重要的是,由於這個類在整個生命周期中只會被加載一次,只會被創建一次,因此惡漢式單例是線程安全的。
那餓漢式單例為什麼是天生就線程安全呢?
因為類加載的方式是按需加載,且只加載一次。由於一個類在整個生命周期中只會被加載一次,在線程訪問單例對象之前就已經創建好了,且僅此一個實例。即線程每次都只能也必定只可以拿到這個唯一的對象。
懶漢式單例
// 懶漢式單例
public class LazySingleton {
// 私有靜態實例引用
private static LazySingleton singleton;
// 私有的構造方法
private LazySingleton(){}
// 返回靜態實例的靜態公有方法,靜態工廠方法
public static LazySingleton getSingleton(){
//當需要創建類的時候創建單例類,並將引用所指向的實例
if (singleton == null) {
singleton = new LazySingleton();
}
return singleton;
}
}
懶漢式單例是延遲加載,只有在需要使用的時候才會實例化一個對象,並將引用所指向的這個對象。
由於是需要時創建,在多線程環境是不安全的,可能會併發創建實例,出現多實例的情況,單例模式的初衷是相背離的。那我們需要怎麼避免呢?可以看接下來的多線程中單例模式的實現形式。
那為什麼傳統的懶漢式單例為什麼是非線程安全的?
非線程安全主要原因是,會有多個線程同時進入創建實例(if (singleton == null) {}代碼塊)的情況發生。當這種這種情形發生后,該單例類就會創建出多個實例,違背單例模式的初衷。因此,傳統的懶漢式單例是非線程安全的。
多線程實現方式
在單線程環境下,無論是餓漢式單例還是懶漢式單例,它們都能夠正常工作。但是,在多線程環境下就有可能發生變異:
那我們應該怎麼在懶漢的基礎上改造呢?
-
synchronized方法
-
synchronized塊
-
使用內部類實現延遲加載
synchronized方法
// 線程安全的懶漢式單例
public class SynchronizedSingleton {
private static SynchronizedSingleton synchronizedSingleton;
private SynchronizedSingleton(){}
// 使用 synchronized 修飾,臨界資源的同步互斥訪問
public static synchronized SynchronizedSingleton getSingleton(){
if (synchronizedSingleton == null) {
synchronizedSingleton = new SynchronizedSingleton();
}
return synchronizedSingleton;
}
}
使用 synchronized 修飾 getSingleton()方法,將getSingleton()方法進行加鎖,實現對臨界資源的同步互斥訪問,以此來保證單例。
雖然可現實線程安全,但由於同步的作用域偏大、鎖的粒度有點粗,會導致運行效率會很低。
synchronized塊
// 線程安全的懶漢式單例
public class BlockSingleton {
private static BlockSingleton singleton;
private BlockSingleton(){}
public static BlockSingleton getSingleton2(){
synchronized(BlockSingleton.class){ // 使用 synchronized 塊,臨界資源的同步互斥訪問
if (singleton == null) {
singleton = new BlockSingleton();
}
}
return singleton;
}
}
其實synchronized塊跟synchronized方法類似,效率都偏低。
使用內部類實現延遲加載
// 線程安全的懶漢式單例
public class InsideSingleton {
// 私有內部類,按需加載,用時加載,也就是延遲加載
private static class Holder {
private static InsideSingleton insideSingleton = new InsideSingleton();
}
private InsideSingleton() {
}
public static InsideSingleton getSingleton() {
return Holder.insideSingleton;
}
}
雙重檢查(Double-Check idiom)現實
雙重檢查(Double-Check idiom)-volatile
使用雙重檢測同步延遲加載去創建單例,不但保證了單例,而且提高了程序運行效率。
// 線程安全的懶漢式單例
public class DoubleCheckSingleton {
//使用volatile關鍵字防止重排序,因為 new Instance()是一個非原子操作,可能創建一個不完整的實例
private static volatile DoubleCheckSingleton singleton;
private DoubleCheckSingleton() {
}
public static DoubleCheckSingleton getSingleton() {
// Double-Check idiom
if (singleton == null) {
synchronized (DoubleCheckSingleton.class) {
// 只需在第一次創建實例時才同步
if (singleton == null) {
singleton = new DoubleCheckSingleton();
}
}
}
return singleton;
}
}
為了在保證單例的前提下提高運行效率,我們需要對singleton實例進行第二次檢查,為的式避開過多的同步(因為同步只需在第一次創建實例時才同步,一旦創建成功,以後獲取實例時就不需要同步獲取鎖了)。
但需要注意的必須使用volatile關鍵字修飾單例引用,為什麼呢?
如果沒有使用volatile關鍵字是可能會導致指令重排序情況出現,在Singleton 構造函數體執行之前,變量 singleton可能提前成為非 null 的,即賦值語句在對象實例化之前調用,此時別的線程將得到的是一個不完整(未初始化)的對象,會導致系統崩潰。
此可能為程序執行步驟:
-
線程 1 進入 getSingleton() 方法,由於 singleton 為 null,線程 1 進入 synchronized 塊 ;
-
同樣由於 singleton為 null,線程 1 直接前進到 singleton = new DoubleCheckSingleton()處,在new對象的時候出現重排序,導致在構造函數執行之前,使實例成為非 null,並且該實例並未初始化的(原因在NOTE);
-
此時,線程 2 檢查實例是否為 null。由於實例不為 null,線程 2 得到一個不完整(未初始化)的 Singleton 對象;
-
線程 1 通過運行 Singleton對象的構造函數來完成對該對象的初始化。
這種安全隱患正是由於指令重排序的問題所導致的。而volatile 關鍵字正好可以完美解決了這個問題。使用volatile關鍵字修飾單例引用就可以避免上述災難。
NOTE
new 操作會進行三步走,預想中的執行步驟:
memory = allocate(); //1:分配對象的內存空間
ctorInstance(memory); //2:初始化對象
singleton = memory; //3:使singleton3指向剛分配的內存地址
**但實際上,這個過程可能發生無序寫入(指令重排序),可能會導致所下執行步驟:
memory = allocate(); //1:分配對象的內存空間
singleton3 = memory; //3:使singleton3指向剛分配的內存地址
ctorInstance(memory); //2:初始化對象
雙重檢查(Double-Check idiom)-ThreadLocal
藉助於 ThreadLocal,我們可以實現雙重檢查模式的變體。我們將臨界資源線程局部化,具體到本例就是將雙重檢測的第一層檢測條件 if (instance == null) 轉換為 線程局部範圍內的操作 。
// 線程安全的懶漢式單例
public class ThreadLocalSingleton
// ThreadLocal 線程局部變量
private static ThreadLocal<ThreadLocalSingleton> threadLocal = new ThreadLocal<ThreadLocalSingleton>();
private static ThreadLocalSingleton singleton = null;
private ThreadLocalSingleton(){}
public static ThreadLocalSingleton getSingleton(){
if (threadLocal.get() == null) { // 第一次檢查:該線程是否第一次訪問
createSingleton();
}
return singleton;
}
public static void createSingleton(){
synchronized (ThreadLocalSingleton.class) {
if (singleton == null) { // 第二次檢查:該單例是否被創建
singleton = new ThreadLocalSingleton(); // 只執行一次
}
}
threadLocal.set(singleton); // 將單例放入當前線程的局部變量中
}
}
藉助於 ThreadLocal,我們也可以實現線程安全的懶漢式單例。但與直接雙重檢查模式使用,使用ThreadLocal的實現在效率上還不如雙重檢查鎖定。
枚舉實現方式
它不僅能避免多線程同步問題,而且還能防止反序列化重新創建新的對象,
直接通過Singleton.INSTANCE.whateverMethod()的方式調用即可。方便、簡潔又安全。
public enum EnumSingleton {
instance;
public void whateverMethod(){
//dosomething
}
}
測試單例線程安全性
使用多個線程,並使用hashCode值計算每個實例的值,值相同為同一實例,否則為不同實例。
public class Test {
public static void main(String[] args) {
Thread[] threads = new Thread[10];
for (int i = 0; i < threads.length; i++) {
threads[i] = new TestThread();
}
for (int i = 0; i < threads.length; i++) {
threads[i].start();
}
}
}
class TestThread extends Thread {
@Override
public void run() {
// 對於不同單例模式的實現,只需更改相應的單例類名及其公有靜態工廠方法名即可
int hash = Singleton5.getSingleton5().hashCode();
System.out.println(hash);
}
}
小結
單例模式是 Java 中最簡單,也是最基礎,最常用的設計模式之一。在運行期間,保證某個類只創建一個實例,保證一個類僅有一個實例,並提供一個訪問它的全局訪問點 ,介紹單例模式的各種寫法:
各位看官還可以嗎?喜歡的話,動動手指點個,點個關注唄!!謝謝支持! 歡迎關注公眾號【Ccww技術博客】,原創技術文章第一時間推出
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面
※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!
※想知道最厲害的網頁設計公司"嚨底家"!
※別再煩惱如何寫文案,掌握八大原則!
※產品缺大量曝光嗎?你需要的是一流包裝設計!
※回頭車貨運收費標準
※台中搬家公司費用怎麼算?