1. 程式人生 > >單例模式的懶漢式及其中的執行緒安全問題

單例模式的懶漢式及其中的執行緒安全問題

先看程式碼:

複製程式碼

package com.roocon.thread.t5;

public class Singleton2 {

    private Singleton2(){

    }

    private static Singleton2 instance;

    public static Singleton2 getInstance(){
        if(instance == null) {//1:讀取instance的值
            instance = new Singleton2();//2: 例項化instance
        }
        return instance;
    }

}

複製程式碼

複製程式碼

package com.roocon.thread.t5;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class MultiThreadMain {
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newFixedThreadPool(20);
        for (int i = 0; i< 20; i++) {
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName()+":"+Singleton2.getInstance());
                }
            });
        }
     threadPool.shutdown();
} }

複製程式碼

執行結果:

複製程式碼

pool-1-thread-4:[email protected]
pool-1-thread-14:[email protected]
pool-1-thread-10:[email protected]
pool-1-thread-8:[email protected]
pool-1-thread-5:[email protected]
pool-1-thread-12:[email protected]
pool-1-thread-1:[email protected]
pool-1-thread-9:[email protected] pool-1-thread-6:[email protected] pool-1-thread-2:[email protected] pool-1-thread-16:[email protected] pool-1-thread-3:[email protected] pool-1-thread-17:[email protected] pool-1-thread-13:[email protected] pool-1-thread-18:[email protected] pool-1-thread-7:[email protected] pool-1-thread-20:[email protected] pool-1-thread-11:[email protected] pool-1-thread-15:[email protected] pool-1-thread-19:[email protected]

複製程式碼

發現,有個例項是[email protected],也就說明,返回的不是同一個例項。這就是所謂的執行緒安全問題。

解釋原因:對於以上程式碼註釋部分,如果此時有兩個執行緒,執行緒A執行到1處,讀取了instance為null,然後cpu就被執行緒B搶去了,此時,執行緒A還沒有對instance進行例項化。

因此,執行緒B讀取instance時仍然為null,於是,它對instance進行例項化了。然後,cpu就被執行緒A搶去了。此時,執行緒A由於已經讀取了instance的值並且認為它為null,所以,

再次對instance進行例項化。所以,執行緒A和執行緒B返回的不是同一個例項。

那麼,如何解決呢?

1.在方法前面加synchronized修飾。這樣肯定不會再有執行緒安全問題。

複製程式碼

package com.roocon.thread.t5;

public class Singleton2 {

    private Singleton2(){

    }

    private static Singleton2 instance;

    public static synchronized Singleton2 getInstance(){
        if(instance == null) {//1
            instance = new Singleton2();//2
        }
        return instance;
    }

}

複製程式碼

但是,這種解決方式,假如有100個執行緒同時執行,那麼,每次去執行getInstance方法時都要先獲得鎖再去執行方法體,如果沒有鎖,就要等待,耗時長,感覺像是變成了序列處理。因此,嘗試其他更好的處理方式。

2. 加同步程式碼塊,減少鎖的顆粒大小。我們發現,只有第一次instance為null的時候,才去建立例項,而判斷instance是否為null是讀的操作,不可能存線上程安全問題,因此,我們只需要對建立例項的程式碼進行同步程式碼塊的處理,也就是所謂的對可能出現執行緒安全的程式碼進行同步程式碼塊的處理。

複製程式碼

package com.roocon.thread.t5;

public class Singleton2 {

    private Singleton2(){

    }

    private static Singleton2 instance;

    public static Singleton2 getInstance(){
        if(instance == null) {
            synchronized (Singleton2.class){
                instance = new Singleton2();
            }
        }
        return instance;
    }

}

複製程式碼

但是,這樣處理就沒有問題了嗎?同樣的原理,執行緒A和執行緒B,執行緒A讀取instance值為null,此時cpu被執行緒B搶去了,執行緒B再來判斷instance值為null,於是,它開始執行同步程式碼塊中的程式碼,對instance進行例項化。此時,執行緒A獲得cpu,由於執行緒A之前已經判斷instance值為null,於是開始執行它後面的同步程式碼塊程式碼。它也會去對instance進行例項化。

這樣就導致了還是會建立兩個不一樣的例項。

那麼,如何解決上面的問題。

很簡單,在同步程式碼塊中instance例項化之前進行判斷,如果instance為null,才對其進行例項化。這樣,就能保證instance只會例項化一次了。也就是所謂的雙重檢查加鎖機制。

再次分析上面的場景:

執行緒A和執行緒B,執行緒A讀取instance值為null,此時cpu被執行緒B搶去了,執行緒B再來判斷instance值為null。於是,它開始執行同步程式碼塊程式碼,對instance進行了例項化。這是執行緒A獲得cpu執行權,當執行緒A去執行同步程式碼塊中的程式碼時,它再去判斷instance的值,由於執行緒B執行完後已經將這個共享資源instance例項化了,所以instance不再為null,所以,執行緒A就不會再次實行例項化程式碼了。

複製程式碼

package com.roocon.thread.t5;

public class Singleton2 {

    private Singleton2(){

    }

    private static Singleton2 instance;

    public static synchronized Singleton2 getInstance(){
        if(instance == null) {
            synchronized (Singleton2.class){
                if (instance == null){
                    instance = new Singleton2();
                }
            }
        }
        return instance;
    }

}

複製程式碼

但是,雙重檢查加鎖並不程式碼百分百一定沒有執行緒安全問題了。因為,這裡會涉及到一個指令重排序問題。instance = new Singleton2()其實可以分為下面的步驟:

1.申請一塊記憶體空間;

2.在這塊空間裡實例化物件;

3.instance的引用指向這塊空間地址;

指令重排序存在的問題是:

對於以上步驟,指令重排序很有可能不是按上面123步驟依次執行的。比如,先執行1申請一塊記憶體空間,然後執行3步驟,instance的引用去指向剛剛申請的記憶體空間地址,那麼,當它再去執行2步驟,判斷instance時,由於instance已經指向了某一地址,它就不會再為null了,因此,也就不會例項化物件了。這就是所謂的指令重排序安全問題。那麼,如何解決這個問題呢?

加上volatile關鍵字,因為volatile可以禁止指令重排序。

複製程式碼

package com.roocon.thread.t5;

public class Singleton2 {

    private Singleton2(){

    }

    private static volatile Singleton2 instance;

    public static Singleton2 getInstance(){
        if(instance == null) {
            synchronized (Singleton2.class){
                if (instance == null){
                    instance = new Singleton2();
                }
            }
        }
        return instance;
    }

}

複製程式碼