第六章:單利模式,懶漢式,餓漢式以及靜態內部類,雙重檢查
阿新 • • 發佈:2018-12-26
文章轉自:https://blog.csdn.net/u012453843/article/details/73743997
單例模式,最常見的就是飢餓模式和懶漢模式,一個直接例項化物件,一個在呼叫方法時進行例項化物件。
大家見的最多的莫過於下面這種單例模式了,這種模式是懶漢模式,就是說只有你呼叫getInstance方法的時候,它才會建立例項。但是這種方式有個非常致命的問題就是在多執行緒的情況下不能正常工作。
- public class Singleton {
- private static Singleton instance;
- private Singleton (){}
- public static Singleton getInstance() {
- if (instance == null) {
- instance = new Singleton();
- }
- return instance;
- }
- }
- public class Singleton {
- private static Singleton instance;
- private Singleton (){}
- public static synchronized Singleton getInstance() {
- if (instance == null) {
- instance = new Singleton();
- }
- return instance;
- }
- }
- public class Singleton {
- private static Singleton instance = new Singleton();
- private Singleton (){}
- public static Singleton getInstance() {
- return instance;
- }
- }
第一種方式:靜態內部類
這種方式是最好的單例模式,而且還是執行緒安全的,為何這麼說呢,靜態內部類Singleton在初始化過程中是不會被載入的,只有當用戶呼叫共用的getInstance方法時才會載入內部類Singleton並且例項化Singleton例項,也就是說,靜態內部類這種方式也屬於懶漢模式,只是實現方式不一樣而已。之所以是執行緒安全的,是因為Singleton是靜態的,靜態內部類只會被例項化一次,也就是說不管有多少執行緒,大家拿到的是同一個例項,不會再去進行多次例項化,從而達到了執行緒安全的目的。由於沒有加鎖,所以併發性特別高,執行緒還安全,所以大家以後碰到單例模式,用靜態內部類最為合適。如下圖所示。
程式碼如下:
- package com.internet.singleton;
- public class InnerSingleton {
- private static class Singleton{
- private static InnerSingleton single = new InnerSingleton();
- }
- public static InnerSingleton getInstance(){
- return Singleton.single;
- }
- }
首先,我說一下這種設計的初衷是什麼,我們看到了getDs()方法中添加了類鎖(synchronized (DubbleSingleton.class)),這種鎖直接把整個類都鎖住了,其它執行緒訪問這個類的任何方法都要排隊等候,設計者目的是當第一個執行緒訪問getDs()方法時,它把整個類鎖住,然後它把這個類例項化,然後後面的執行緒進入getDs()方法後一判斷髮現ds已經不是null了,於是便把第一個執行緒例項化好的例項返回。這樣後續的執行緒無論併發有多少都沒有問題,也不用再排隊,直接拿取例項化好的例項即可。既然這樣,為何在synchronized (DubbleSingleton.class)當中再判斷一次ds是否為null呢?這個其實也好理解,synchronized上面不是模擬了一下初始化物件的準備時間嗎?這個模擬是很有必要的,當第一個執行緒進入getDs()方法後,判斷了一下df是否為null,發現是null,然後它就進入初始化物件的準備工作中去了,這個過程可能需要幾秒鐘,注意這時第一個執行緒還沒有執行到synchronized這一行程式碼,也就是說還沒有加上類鎖,在這個過程當中假如又有多個執行緒要呼叫getDs()方法,它們便可以同時訪問這個方法,這些執行緒判斷ds依然是null,因此會進入到if判斷裡面,這些執行緒也進入到初始化物件的準備過程,等到第一個執行緒初始化物件準備完畢之後,它進入到synchronized這塊程式碼處,給整個類加上了鎖,這時後續再有執行緒的話,就要排隊等候呼叫getDs()了。等第一個執行緒執行完例項化程式碼之後,剛才那些趁第一個執行緒沒有鎖住類的間隙偷偷摸進來的執行緒便也都進入到synchronized程式碼處,這些執行緒會依次鎖住類進入到synchronized程式碼塊中,這時如果程式碼塊中不加一層if判斷的話,就會再例項化一次DubbleSingleton類並返回,有幾個這樣偷溜進來的執行緒便會例項化幾次Dubblesingleton例項。除了這些偷溜進來的執行緒之外,再來訪問的執行緒由於在類外等待第一個執行緒執行完之後才有機會進入到方法體中,這時ds早已經例項化過了,因此第一個if判斷便不成立,於是直接把當前Dubblesingleton例項返回。也就是說,後面的執行緒便可以高併發了,不用受鎖的限制了。而如果在synchronized鎖內再加一層判斷的話,由於第二個執行緒進入鎖內時,第一個執行緒肯定已經執行完了(synchronized這時扮演的是類鎖),因此這時ds肯定不是null了,第二個執行緒一判斷,發現ds不為null了,便直接把第一個執行緒例項化好的例項返回了,同理,其它執行緒也把第一個執行緒例項化好的例項返回。從而保證了執行緒安全。
雙重檢查與懶漢加鎖模式最大的區別在於,雙重檢查加鎖只是一瞬間的事兒,後續無論有多少個執行緒都可以自由訪問,沒有執行緒安全問題。而懶漢加鎖模式由於每個執行緒都要排隊訪問getInstance方法,因此效率太低。這就是他們之間的區別。雙重檢查的程式碼類如下圖所示。
程式碼如下:
- package com.internet.singleton;
- public class DubbleSingleton {
- private static DubbleSingleton ds;
- public static DubbleSingleton getDs(){
- if(ds == null){
- try {
- //模擬初始化物件的準備時間...
- Thread.sleep(3000);
- } catch (Exception e) {
- e.printStackTrace();
- }
- synchronized (DubbleSingleton.class) {
- if(ds == null){
- ds = new DubbleSingleton();
- }
- }
- }
- return ds;
- }
- public static void main(String[] args){
- Thread t1 = new Thread(new Runnable() {
- @Override
- public void run() {
- System.out.println(DubbleSingleton.getDs().hashCode());
- }
- },"t1");
- Thread t2 = new Thread(new Runnable() {
- @Override
- public void run() {
- System.out.println(DubbleSingleton.getDs().hashCode());
- }
- },"t2");
- Thread t3 = new Thread(new Runnable() {
- @Override
- public void run() {
- System.out.println(DubbleSingleton.getDs().hashCode());
- }
- },"t3");
- t1.start();
- t2.start();
- t3.start();
- }
- }
如果把synchronized程式碼塊中的if判斷去掉,如下圖所示。
再執行main方法,結果如下圖所示,發現三次結果都不一樣,說明有執行緒安全問題。
綜合以上所有情況,可以知道,用靜態內部類是最好的單例模式。