在面試的時候面試官會怎麼在單例模式中提問呢?你又該如何回答呢?可能你在面試的時候你會碰到這些問題:java
那咱們該怎麼回答呢?那答案來了,看完接下來的內容就能夠跟面試官嘮嘮單例模式了面試
單例模式是一種經常使用的軟件設計模式,其屬於建立型模式,其含義便是一個類只有一個實例,併爲整個系統提供一個全局訪問點 (向整個系統提供這個實)。設計模式
結構: 安全
單例模式三要素:多線程
在使用單例模式時,咱們必須使用單例類提供的公有工廠方法獲得單例對象,而不該該使用反射來建立,使用反射將會破壞單例模式 ,將會實例化一個新對象。併發
在單線程環境下,單例模式根據實例化對象時機的不一樣分爲,ide
從速度和反應時間角度來說,餓漢式(又稱當即加載)要好一些;從資源利用效率上說,懶漢式(又稱延遲加載)要好一些。函數
// 餓漢式單例 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) {}代碼塊)的狀況發生。當這種這種情形發生後,該單例類就會建立出多個實例,違背單例模式的初衷。所以,傳統的懶漢式單例是非線程安全的。
在單線程環境下,不管是餓漢式單例仍是懶漢式單例,它們都可以正常工做。可是,在多線程環境下就有可能發生變異:
那咱們應該怎麼在懶漢的基礎上改造呢?
// 線程安全的懶漢式單例 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()方法進行加鎖,實現對臨界資源的同步互斥訪問,以此來保證單例。
雖然可現實線程安全,但因爲同步的做用域偏大、鎖的粒度有點粗,會致使運行效率會很低。
// 線程安全的懶漢式單例 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; } }
使用雙重檢測同步延遲加載去建立單例,不但保證了單例,並且提升了程序運行效率。
// 線程安全的懶漢式單例 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 的,即賦值語句在對象實例化以前調用,此時別的線程將獲得的是一個不完整(未初始化)的對象,會致使系統崩潰。
此可能爲程序執行步驟:
這種安全隱患正是因爲指令重排序的問題所致使的。而volatile 關鍵字正好能夠完美解決了這個問題。使用volatile關鍵字修飾單例引用就能夠避免上述災難。
NOTE
new 操做會進行三步走,預想中的執行步驟:
memory = allocate(); //1:分配對象的內存空間 ctorInstance(memory); //2:初始化對象 singleton = memory; //3:使singleton3指向剛分配的內存地址但實際上,這個過程可能發生無序寫入(指令重排序),可能會致使所下執行步驟:
memory = allocate(); //1:分配對象的內存空間 singleton3 = memory; //3:使singleton3指向剛分配的內存地址 ctorInstance(memory); //2:初始化對象
藉助於 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 中最簡單,也是最基礎,最經常使用的設計模式之一。在運行期間,保證某個類只建立一個實例,保證一個類僅有一個實例,並提供一個訪問它的全局訪問點 ,介紹單例模式的各類寫法:
懶漢式單例
使用雙重檢查模式
各位看官還能夠嗎?喜歡的話,動動手指點個💗,點個關注唄!!謝謝支持!
歡迎掃碼關注,原創技術文章第一時間推出
![]()