單例模式中用volatile和synchronized來滿足雙重檢查鎖機制


背景:我們在實現單例模式的時候往往會忽略掉多線程的情況,就是寫的代碼在單線程的情況下是沒問題的,但是一碰到多個線程的時候,由於代碼沒寫好,就會引發很多問題,而且這些問題都是很隱蔽和很難排查的。

例子1:沒有volatile修飾的uniqueInstance

public class Singleton {
    private static Singleton uniqueInstance;

    private Singleton(){
    }

    public static Singleton getInstance(){
        if(uniqueInstance == null){ //#1
            synchronized(Singleton.class){ //#2
                if(uniqueInstance == null){ //#3
                    uniqueInstance = new Singleton(); //#4
                    System.out.println(Thread.currentThread().getName() + ": uniqueInstance is initalized..."); //#5.1
                } else {
                    System.out.println(Thread.currentThread().getName() + ": uniqueInstance is not null now..."); //#5.2
                }
            }
        }
        return uniqueInstance;
    }
}
 1 public class TestSingleton {
 2     public static void main(final String[] args) throws InterruptedException {
 3         for (int i = 1; i <= 100000; i++) {
 4             final Thread t1 = new Thread(new ThreadSingleton());
 5             t1.setName("thread" + i);
 6             t1.start();
 7         }
 8     }
 9 
10     public static class ThreadSingleton implements Runnable {
11         @Override
12         public void run() {
13             Singleton.getInstance();
14         }
15     }
16 }

這里面的結果有可能會是:(沒有真正重現過,太難模擬了)

1 thread2: uniqueInstance is initalized...
2 thread3: uniqueInstance is initalized...
Singleton被實例化兩次了,和我們的單例模式設計期望值不一致:類永遠只被實例化一次.

原因分析:
1. thread2進入#1, 這時子線程的uniqueInstance都是為空的,thread2讓出CPU資源給thread3
2. thread3進入#1, 這時子線程的uniqueInstance都是為空的, thread3讓出CPO資源給thread2
3. thread2會依次執行#2,#3,#4, #5.1,最終在thread2里面實例化了uniqueInstance。thread2執行完畢讓出CPO資源給thread3
4. thread3接着#1跑下去,跑到#3的時候,由於#1里面拿到的uniqueInstance還是空(並沒有及時從thread2里面拿到最新的),所以thread3仍然會執行#4,#5.1
5. 最后在thread2和thread3都實例化了uniqueInstance

例子2:用volatile修飾的uniqueInstance

這里就不貼重復的代碼了,因為只是加多一個volatile來修飾成員變量:uniqueInstance,

但是結果卻是正確的了, 其中一個可能結果:

 

thread2: uniqueInstance is initalized
thread3: uniqueInstance is not null now...

 

原因分析:

volatile(java5):可以保證多線程下的可見性;

讀volatile:每當子線程某一語句要用到volatile變量時,都會從主線程重新拷貝一份,這樣就保證子線程的會跟主線程的一致。

寫volatile: 每當子線程某一語句要寫volatile變量時,都會在讀完后同步到主線程去,這樣就保證主線程的變量及時更新。

1. thread2進入#1, 這時子線程的uniqueInstance都是為空的(java內存模型會從主線程拷貝一份uniqueInstance=null到子線程thread2),thread2讓出CPU資源給thread3
2. thread3進入#1, 這時子線程的uniqueInstance都是為空的(java內存模型會從主線程拷貝一份uniqueInstance=null到子線程thread2), thread3讓出CPO資源給thread2
3. thread2會依次執行#2,#3,#4, #5.1,最終在thread2里面實例化了uniqueInstance(由於是volatile修飾的變量,會馬上同步到主線程的變量去)。thread2執行完畢讓出CPO資源給thread3
4. thread3接着#1跑下去,跑到#3的時候,會又一次從主線程拷貝一份uniqueInstance!=null回來,所以thread3就直接跑到了#5.2
5. 最后在thread3不再會重復實例化uniqueInstance了

參考文章:如何在Java中使用雙重檢查鎖實現單例

官方文檔說明

深入理解Java內存模型(一)——基礎

雙重檢查鎖定與延遲初始化


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM