單例模式具有以下特點:
單例類只能有一個實例。單例類必須自己創建自己的唯一實例。單例類必須給所有其他對象提供這一實例。它的作用如下:
某些類創建比較頻繁,對于一些大型的對象,這是一筆很大的系統開銷。
省去了 new 操作符,降低了系統內存的使用頻率,減輕 GC 壓力。
有些類如交易所的核心交易引擎,控制著交易流程,如果該類可以創建多個的話,系統完全亂了。(比如一個軍隊出現了多個司令員同時指揮,肯定會亂成一團),所以只有使用單例模式。
餓漢模式,指在類被加載時(即類對象被調用前)就完成實例化。
這種方式雖然可以保證單例,并了避免多線程安全問題,但是會提前占用系統資源,效率低下。
public class Singleton { // 持有私有靜態實例,防止被引用 // 對變量直接賦值,時類在被加載時就被實例化 PRivate static Singleton instance =new Singleton(); // 私有構造方法,防止被實例化 private Singleton() { } public static Singleton getInstance() { return instance; }}為了解決餓漢模式的效率問題,引入了懶漢模式,該模式指的是在類對象被調用時才創建實例。
雖然懶漢模式解決了效率問題,但又帶來線程安全的問題。
當多線程同時調用 getInstance 方法時,都認為 instance 為空,結果就是都新建了類對象。無法保證單例。
public class Singleton { // 不對變量直接賦值,實現延遲加載 private static Singleton instance; private Singleton() { } public static Singleton getInstance() { // 該變量為空時創建 if (instance == null) { instance = new Singleton(); } return instance; }}普通的懶漢模式既然存在線程安全問題,使用同步關鍵字 synchronized 就方法加鎖能解決。
線程安全的問題是解決了,但是又帶來了新的效率問題。
當多線程調用 getInstance 方法,無論對象是否已被創建,它都會被鎖住。
public class Singleton { private static Singleton instance; private Singleton() { } // 對方法加鎖 public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; }}當多線程調用 getInstance 方法,如果對方法加鎖,就會造成效率問題。
而加鎖的主要目的是為了保證創建對象時的線程安全。那么可以縮小同步區域,在新建對象時對其加鎖,存在對象時則不需要加鎖。
public class Singleton { private static Singleton instance; private Singleton() { } public static synchronized Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { // 之所以要再次檢查,因為當線程排序進入同步塊時。 // 第一個線程創建了實例,而后面進入同步塊的線程就必須立即返回 if(instance == null){ instance = new Singleton(); } } } return instance; }}采用雙重檢查鎖定,貌似是解決了已經問題了。但其實不然。原因就于新建類實例時的操作不是原子性的。
// 該語句實際包含了三條指令:// 1.給 Singleton 實例分配內存。// 2.初始化 Singleton 構造器 // 3.將instance 對象指向分配的內存空間(注意到這步 instance 就非null了)。 instance = new Singleton();在 java 內存模型中,允許編譯器和處理器對指令進行重排序,但是它會保證程序最終結果會和代碼順序執行結果相同。
所以執行結果可以是 1->2->3,也可是 1->3->2。這種重排序在單線程并不會對線程造成影響,但是在多線程是就會出現問題:
線程1、2 同時訪問同步代碼塊。線程1 線程先進入,線程2 則等待。線程1 創建實例后離開(此時指令執行為 1->3->2)。線程2 進入判斷實例不為空,但是線程對于的 Singleton 初始化構造器還沒完成,導致程序錯誤。而利用 volatile 修飾變量可以保證它的可見性、有序性,從而讓執行順序為 1->2->3,避免了上述問題。
public class Singleton { // 利用 volatile 關鍵字修飾變量 private volatile static Singleton instance; private Singleton() { } public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if(instance == null){ instance = new Singleton(); } } } return instance; }}重新分析以上單例模式會帶來的問題:
使用餓漢模式會造成對象提前實例化,占用系統資源。使用懶漢模式會解決了系統資源占用,但會造成線程安全問題。使用 synchronized 、volatile 解決了線程安全問題,但這樣實現的效率其實也不高。所以需要解決的問題有三:系統資源占用、線程安全、執行效率。
因此這里使用靜態內部類的方式實現,因為:
不提前占用系統資源:當調用 getInstance 時會訪問到 SingletonHolder.instance,此時內部類才被加載并初始化靜態變量,創建實例。
不存在線程安全問題:JVM 內部的機制能夠保證當一個類被加載的時候,這個類的加載過程是線程互斥的。這樣當第一次調用 getInstance 時,JVM能夠保證 instance 只被創建一次,并且會保證把賦值給 instance 的內存初始化完畢。
解決效率問題:只有在 getInstance 第一次被調用時,由 JVM 實現互斥,這樣就解決了低性能問題。
public class Singleton { // 創建靜態內部類 private static class SingletonHolder{ private static Singleton instance= new Singleton(); } private Singleton() { } public static Singleton getInstance() { return SingletonHolder.instance; }}新聞熱點
疑難解答