在項目開發中常常能碰見的設計模式就是單例模式了,而實現的方式最多見的有兩種:餓漢和飽漢(懶漢)。因爲平常接觸較多而研究的不夠深刻,致使面試的時候被詢問到後有點沒底,這裏記錄一下學習的過程。java
餓漢的名字由來就是由於很餓很着急,因此在類加載時即建立實例對象,實現以下:面試
public class Singleton { private static final Singleton singleton = new Singleton(); private Singleton(){ } public static Singleton getInstance(){ return singleton; }
餓漢模式自己就是線程安全的,爲何是線程安全的呢?緣由是這樣的,JVM虛擬機在執行類加載的初始化階段,能保證一個類的<clinit>方法在多線程環境下可以被正確的加鎖,同步,若是多線程初始化一個類,那麼只有一個線程會去執行這個類的<clinit>方法,其餘須要阻塞,更況且咱們還加入了final關鍵字,若是某個成員是final的,JVM規範作出以下明確的保證:一旦對象引用對其餘線程可見,則其final成員也必須正確的賦值了。設計模式
所以居於上述兩點可以保證餓漢單例正確的在多線程環境下運行。安全
飽漢的實現跟餓漢不一樣,飽漢只在調用獲取實例的時候纔會進行new對象的過程,簡單的實現以下:多線程
public class Singleton { private static Singleton singleton; private Singleton() { } public static Singleton getInstance() { if (singleton == null) { singleton = new Singleton(); } return singleton; } }
在單線程的環境中,使用該模式是徹底沒有問題的,不會涉及到臨界問題,而在多線程模式下,那麼就不能保證了。假設有兩個線程A和B,A線程判斷singleton==null了,這時候進行singleton = new Singleton()操做,在該步尚未完成時,線程B進入了方法體中,判斷singleton==null,因爲A尚未實例化完成Singleton,致使singleton==null成立,B線程也執行了singleton = new Singleton()的操做,那麼就不能保證在只有單次賦值的狀況了,也就不能保證每一個線程中的Singleton對象是同樣的。併發
那麼改進方式也很簡單,既然有臨界問題,那麼咱們就加個鎖來保證線程的安全性問題:app
public class Singleton { private static Singleton singleton; private Singleton() { } public static Singleton getInstance() { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } return singleton; } }
這個方式就能保證單例模式的正常使用了,可是因爲咱們每次調用getInstance()的時候都要進行加鎖/解鎖的操做,在多線程中,在CPU調度切換不一樣線程時候會發生上下文切換,上下文切換時候,JVM須要去保存當前線程對應的寄存器使用狀態,以及代碼執行的位置等等,那麼確定是會有必定的開銷的。並且當線程因爲等待某個鎖而被阻塞的時候,JVM一般將該線程掛起,掛起線程和恢復線程都是須要轉到內核態中進行,頻繁的進行用戶態到內核態的切換對於操做系統的併發性能來講會形成不小的壓力。所以上面的寫法實際上相對來講較爲低效,那麼,這個時候咱們進行優化變成以下代碼:性能
public class Singleton { private static Singleton singleton; private Singleton() { } public static Singleton getInstance() { if (singleton == null) {//1 synchronized (Singleton.class) { if (singleton == null) {//2 singleton = new Singleton(); } } } return singleton; } }
在調用synchronized前提早判斷一步是否singleton == null,若是不等於null,那麼說明已經賦值成功,若是等於null,那麼在執行加鎖操做就能夠了。因此加兩次判空的主要緣由就是由於避免重複加/解鎖的操做,浪費系統資源。學習
那麼上面的實現還會不會有問題呢?首先分析一下 singleton = new Singleton()
這句話底層執行的過程:優化
在堆中分配Singleton對象內存
填充Singleton對象的必要信息+具體數據初始化+末位填充
把singleton引用指向這個對象的堆內地址
自己 singleton = new Singleton()
不是一個原子操做,實例化過程會通過上面的三個步驟,並且JVM在遵照as-if-serial語義的狀況下,容許進行指令重排序的過程,也就是能夠執行1-3-2的操做的。
那麼在一些極端的狀況就可能會出現問題:
singleton = new Singleton()
中,這時候JVM進行了重排序優化1-3-2的過程。解決方式也簡單,使用volatile,經過volatile的語義禁止指令重排序功能,那麼就解決了上面的問題了,正確代碼以下:
public class Singleton { private static volatile Singleton singleton; private Singleton() { } public static Singleton getInstance() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }