單例模式
設計模式的概念
設計模式好比象棋中的"棋譜".紅方當頭炮,黑方馬來跳.針對紅方的一些走法,黑方應招的時候有一些固定的套路.按照套路來走局勢就不會吃虧.
軟件開發中也有很多常見的"問題場景".針對這些問題的場景,大佬們總結出了一些固定的套路.按照這些套路來實現代碼,也不會吃虧
單例模式概念
單例 = 單個實例(對象)
具體來說,就是某個類,在一個進程中,只應該創建出一個實例.(也就是原則上不應該有多個)
使用單例模式,就可對代碼進行更嚴格的校驗與檢查.
期望讓機器(編譯器)能夠對代碼中指定的類,創建的實例個數,進行校驗.如果發現創建多個實例了,就直接讓編譯器報錯這種~~
這一點在很多場景上都需要,一般就是一個對象持有(管理)大量數據時,比如JDBC中的DataSource實例只需要一個.
單例模式具體的實現方式有很多.最常見的是"餓漢"和"懶漢"兩種.
餓漢模式
類加載的同時,創建實例.
也就是說實例在類加載的時候就創建了,創建時機非常早,相當于程序一啟動,實例就創建了.
class Singleton {private static Singleton instance = new Singleton();private Singleton(){}public static Singleton getInstance() {return instance;}
}public class TestSingleton {public static void main(String[] args) {Singleton.getInstance();Singleton s = new Singleton();}
}
1.instance是Singleton類對象里持有的屬性.類對象是指Singleton.class(就是從.class加載至內存中,表示類的一個數據結構).
2.private Singleton() {} 是在設置私有構造方法,保證其它代碼不能創建出新的對象.
比如:Singleton s = new Singleton();在這里就無法執行
3.其它代碼如果想要獲得這個類的唯一實例,就可以通過getInstance()方法獲取.
對于餓漢來說,getInstance直接返回Instance實例,這個操作本質上是"讀操作",多個線程讀取同一個變量,是線程安全的.?
懶漢模式-單線程版
類加載的時候不創建實例.第一次使用的時候才創建實例.
class Singleton {private static Singleton instance = null;//這個引用先初始化為null,而不是立即創建實例.private Singleton() {}public static Singleton getInstance() {if(instance == null) {instance = new Singleton();}return instance;}
}
在這個代碼中,首次調用getInstance時,instance引用為null.進入里面的if條件,把實例創建出來.如果后續再次調用,if就不進入.而是直接返回之前創建的引用了.
這樣設定,仍可以保證該類的實例是唯一一個.于此同時,創建實例的時機就不是程序驅動的了,而是第一次調用getInstance時(操作執行時機看程序具體需求.大概率要比餓漢這種方式要晚一些,甚至有可能整個程序壓根用不到這個方法,也就把創建的操作給省下了).?
注意:懶漢模式是比餓漢模式更好一些的.
在計算機中,懶的思想非常有意義:
比如有一個非常大的文件(10GB).有一個編輯器,使用編輯器打開這個文件.
如果是按照"餓漢模式",編輯器就會先把這10GB的數據加載到內存中,然后再進行統一的展示.(即使加載了這么多數據,用戶還得一點一點看,沒法一下子看完這么多..)
如果是按照"懶漢模式",編輯器就會只讀取一小部分數據(比如只讀10KB),把這10KB先展示出來.隨著用戶進行翻頁之類的操作,再繼續讀后續的數據.
懶漢模式-多線程版
?上面的懶漢模式是線程不安全的.
線程安全發生在首次創建實例時.如果在多個線程中同時調用getInstance方法,就可能導致創建出多個實例.
一旦實例已經創建好了,后面再多線程環境調用getInstance就不再有線程安全問題了(不再修改Instance了).
舉個例子:
譬如這種情況,兩次的if條件都符合,會創建兩個實例,顯然不符合規定.
而這時就很容易想到使用synchronized來解決這個問題.
class Singleton {private static Object locker = new Object();private static Singleton instance = null;private Singleton() {}public static Singleton getInstance() {synchronized(locker) {if(instance == null) {instance = new Singleton();}}return instance;}
}
這樣寫確實可以解決線程安全的問題.但還是有一個問題:
比如Instance已經創建過了.此時后續再調用getInstance就都是返回Instance實例了吧(于是此處的操作就是純粹的讀操作了,也就不會有線程安全問題了).
此時,針對這個已經沒有線程安全問題的代碼,仍然時每次調用都先加鎖再解鎖,此時效率就非常低了!!!(加鎖意味著會產生阻塞,一旦線程阻塞,啥時候能解除,就不知道了.你可以認為:只要一個代碼里加鎖了,基本注定就要和"高性能"無緣).?
因此我們說,在不該加鎖的時候是不能亂加的.
解決方案:可以在加鎖外面再套一層if,以判斷是否加鎖.(如果instance為null,說明是首次調用,首次調用就需要考慮線程安全問題->要加鎖 / 如果非null,說明是后續調用->不必加鎖)
再來看一下修改的代碼:
?
class Singleton {private static Object locker = new Object();private static Singleton instance = null;private Singleton() {}public static Singleton getInstance() {if (instance == null) {//第一個if判定的是是否加鎖(保證執行效率)synchronized(locker) {if(instance == null) {//第二個if判定的是是否要創建對象(保障線程安全)instance = new Singleton();}}}return instance;}
}?
但是又雙有一個問題,就是指令重排序引起的線程安全問題.
我們知道,指令重排序,也是編譯器優化的一種方式.(調整原有代碼的執行順序,保證邏輯不變的前提下,提高程序的效率).
這里指的就是instance = new Singleton();?
這條語句可以拆分成多個指令:(1)申請一段內存空間 (2)在內存上調用構造方法,創建出這個實例 (3)把這個內存地址賦給Instance引用變量
正常情況下:是按照(1)(2)(3)順序執行的,但編譯器也可優化成(1)(3)(2)執行,多線程指令重排序可能有問題.
原因如下:
解決方案:給instance加上volatile(volatile可以防止指令重排序).
加上之后,針對這個變量的讀寫操作,就不會出現指令重排序了.
最后代碼如下:
?
class Singleton {private static Object locker = new Object();private static volatile Singleton instance = null;private Singleton() {}public static Singleton getInstance() {if (instance == null) {//第一個if判定的是是否加鎖(保證執行效率)synchronized(locker) {if(instance == null) {//第二個if判定的是是否要創建對象(保障線程安全)instance = new Singleton();}}}return instance;}
}
?
?
?