并發(fā)線程-雙重檢查鎖定問(wèn)題

雙重檢查鎖定問(wèn)題:Double-checked Locking

1、先來(lái)看問(wèn)題代碼

有線程安全問(wèn)題的代碼塊-雙重檢查鎖定問(wèn)題

- 代碼

    // 單例模式構(gòu)建對(duì)象
    public static LocalCache getInstance(){
        // 雙重檢測(cè)鎖,提高運(yùn)行效率
        if (instance == null){
            synchronized (LocalCache.class){
                if (instance == null) {
                    instance = new LocalCache();
                }
            }
        }
        return instance;
    }

代碼(該方法)的預(yù)期目標(biāo)是使用 懶漢模式 的單例設(shè)計(jì)模式獲取對(duì)象,阿里插件顯示這段代碼是線程不安全的

2、改進(jìn)方案

2.1、懶漢模式改為餓漢模式

這種思路有幾種實(shí)現(xiàn)方式,這里貼一種,是靜態(tài)代碼塊實(shí)例化一次對(duì)象

懶漢模式的一種實(shí)現(xiàn)方式

- 代碼

    static {
        instance = new LocalCache();
    }

    // 單例模式構(gòu)建對(duì)象
    public static LocalCache getInstance(){
        return instance;
    }

2.2、同為懶漢模式下的代碼改進(jìn)

可行的方案
instance變量加上 volatile 關(guān)鍵字,保證變量值的可見(jiàn)性

    /**
     * volatile的可見(jiàn)性,可以確保拿到 instance 的最終值
     */
    private static volatile LocalCache instance;

    // 單例模式構(gòu)建對(duì)象
    public static LocalCache getInstance(){
        // 雙重檢測(cè)鎖,提高運(yùn)行效率
        if (instance == null){
            synchronized (LocalCache.class){
                if (instance == null) {
                    instance = new LocalCache();
                }
            }
        }
        return instance;
    }

否定的方案(這個(gè)方案也不能保證線程安全,這里有點(diǎn)不太懂)

  // 單例模式構(gòu)建對(duì)象
    public static LocalCache getInstance(){
        // 雙重檢測(cè)鎖,提高運(yùn)行效率
        if (instance == null){
            synchronized (LocalCache.class){
                if (instance == null) {
                    LocalCache localCache = new LocalCache();
                    instance = localCache;
                }
            }
        }
        return instance;
    }

3、雙重檢測(cè)鎖定問(wèn)題產(chǎn)生的原因

3.1、簡(jiǎn)單來(lái)說(shuō)

instance = new LocalCache();這行代碼有三步操作(《碼出高效Java開(kāi)發(fā)手冊(cè)》P233頁(yè)提到有兩步操作,個(gè)人感覺(jué)不太好解釋):

  • 初始化 LocalCache 實(shí)例
  • 為本來(lái)為null的instance變量開(kāi)辟內(nèi)存空間,并確定默認(rèn)大?。ㄟ@一點(diǎn)《碼出高效》P233頁(yè)書(shū)中并沒(méi)有提到)
  • 將對(duì)象地址寫(xiě)進(jìn) instance 字段
    這三步操作并不是原子化的

3.2、舉個(gè)例子

  • 線程A進(jìn)入到if(instance == null){的時(shí)候,instance為null
  • 線程A進(jìn)入同步代碼塊(synchronized括起來(lái)的代碼塊),到instance = new LocalCache();時(shí)執(zhí)行了 為instance開(kāi)辟內(nèi)存空間將對(duì)象的引用存入內(nèi)存空間 的動(dòng)作,但是沒(méi)有實(shí)例化LocalCache對(duì)象
  • 線程B執(zhí)行到if(instance == null){的時(shí)候,instance不為null(但是實(shí)際沒(méi)有指向某個(gè)堆內(nèi)的內(nèi)存,簡(jiǎn)而言之,這塊內(nèi)存空間(棧的內(nèi)存空間)的引用地址指向的(堆的)內(nèi)存空間中沒(méi)有實(shí)際對(duì)象),所以直接return了一個(gè)中間態(tài)(我自己起的名字。。)的instance
  • 線程B中,接下來(lái)的代碼邏輯中,拿到instance的值其實(shí)是有問(wèn)題的(有啥問(wèn)題?-TODO- 反正是有問(wèn)題的-_-)

這篇blog有相關(guān)介紹
所以這里涉及到指令重排的問(wèn)題(可能也有叫“指令優(yōu)化”的-《碼出高效》P232-P232有提到),即#3.1的三步操作,CPU在執(zhí)行的時(shí)候并不會(huì)根據(jù)代碼里理解的順序(從上到下、從左到右)執(zhí)行,會(huì)判斷怎樣的組合可以提高效率,重新排列指令執(zhí)行的順序(如圖)

指令重排/指令優(yōu)化 導(dǎo)致的線程安全問(wèn)題

3.3、使用了volatile之后

這里用到的是volatile的防止指令重排的能力(JDK1.5之后才有的)-- volatile還有一個(gè)可見(jiàn)性的能力,這里貌似沒(méi)有體現(xiàn)(下篇文章探討volatile 可見(jiàn)性/指令重排 問(wèn)題)

  • 線程A進(jìn)入到if(instance == null){的時(shí)候,instance為null
  • 線程A進(jìn)入同步代碼塊(synchronized括起來(lái)的代碼塊),到instance = new LocalCache();時(shí)執(zhí)行了 為instance開(kāi)辟內(nèi)存空間實(shí)例化LocalCache對(duì)象 的動(dòng)作,但是沒(méi)有將對(duì)象的引用存入內(nèi)存空間
  • 線程B執(zhí)行到if(instance == null){的時(shí)候,instance為null
  • 接下去就是預(yù)期的執(zhí)行流程了

4、復(fù)現(xiàn)雙重檢測(cè)問(wèn)題的方式-供參考

??!實(shí)際復(fù)現(xiàn)過(guò)程中并沒(méi)有復(fù)現(xiàn)問(wèn)題,嚴(yán)重懷疑是復(fù)現(xiàn)方式還可以改進(jìn),以下復(fù)現(xiàn)方式僅供參考

懶漢模式加載的單例對(duì)象類

public class LocalCache {

    private static LocalCache instance;

    // 構(gòu)造方法私有化,防止實(shí)例化
    private LocalCache() {}

    // 單例模式構(gòu)建對(duì)象
    public static LocalCache getInstance() throws InterruptedException {
        // // 雙重檢測(cè)鎖,提高運(yùn)行效率
        if (instance == null){
            synchronized (LocalCache.class){
                if (instance == null) {
                    instance = new LocalCache();
                }
            }
        }
        return instance;
    }
}

建了兩個(gè)線程工廠,每個(gè)工廠里面有兩根線程(總共4根),模擬多線程環(huán)境(有更簡(jiǎn)便的寫(xiě)法)

public class TestJava {
    public static void main(String[] args) {
        BlockingQueue blockingDeque = new LinkedBlockingDeque(2);
        TestThreadFactory firstFactory = new TestThreadFactory("第一個(gè)線程池");
        TestThreadFactory secondFactory = new TestThreadFactory("第二個(gè)線程池");
        TestRejectHandler testRejectHandler = new TestRejectHandler();
        ThreadPoolExecutor firstThreadPool = new ThreadPoolExecutor(2, 2, Integer.MAX_VALUE, TimeUnit.SECONDS, blockingDeque, firstFactory, testRejectHandler);
        ThreadPoolExecutor secondThreadPool = new ThreadPoolExecutor(2, 2, Integer.MAX_VALUE, TimeUnit.SECONDS,
                blockingDeque, secondFactory, testRejectHandler);
        Task task = new Task();
        for (int i = 0; i < 2; i++){
            firstThreadPool.execute(task);
            secondThreadPool.execute(task);
        }
    }

    /**
     * 線程工廠
     */
    public static class TestThreadFactory implements ThreadFactory{

        private final String namePrefix;
        private final AtomicInteger nextId = new AtomicInteger(1);

        public TestThreadFactory(String namePrefix) {
            this.namePrefix = "TestThreadFactory's " + namePrefix + "-worker-";
        }

        @Override
        public Thread newThread(Runnable task) {
            String name = namePrefix + nextId.getAndIncrement();
            Thread thread = new Thread(null, task, name, 0);
            System.out.println(thread);
            return thread;
        }
    }

    /**
     * 實(shí)際執(zhí)行任務(wù)
     */
    public static class Task implements Runnable{

        private final AtomicLong count = new AtomicLong(0L);
        @Override
        public void run() {
            try {
                LocalCache instance = LocalCache.getInstance();
                // todo 這里做一些instance對(duì)象的操作
                System.out.println("running_" + count.getAndIncrement() + ", instance: " + instance);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 當(dāng)線程異常的時(shí)候,可以打印線程異常堆棧
     */
    public static class TestRejectHandler implements RejectedExecutionHandler{

        @Override
        public void rejectedExecution(Runnable task, ThreadPoolExecutor executor) {
            System.out.println("task rejected. " + executor.toString());
        }
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容