單例模式——DCL失效問題

寫在前面:

  • 你好,歡迎關注!
  • 我熱愛技術,熱愛分享,熱愛生活, 我始終相信:技術是開源的,知識是共享的!
  • 博客裏面的內容大部分均爲原創,是自己日常的學習記錄和總結,便於自己在後面的時間裏回顧,當然也是希望可以分享 自己的知識。如果你覺得還可以的話不妨關注一下,我們共同進步!
  • 個人除了分享博客之外,也喜歡看書,寫一點日常雜文和心情分享,如果你感興趣,也可以關注關注!
  • 公衆號:傲驕鹿先生
根據Java語言規範,所有線程在執行Java程序時必須要遵守intra-thread semantics。
intra-thread semantics保證重排序不會改變單線程內的程序執行結果。換句話說,intra-thread semantics允許那些在單線程內,不會改變單線程程序執行結果的重排序。
ourInstance = new Singleton();

創建了一個對象。這一行代碼可以分解爲如下的3行僞代碼:

memory = allocate();  // 1:分配對象的內存空間
ctorInstance(memory); // 2:初始化對象
ourInstance = memory; // 3:設置ourInstance 指向剛分配的內存地址

2和3之間重排序之後的執行時序如下:

memory = allocate();  // 1:分配對象的內存空間
instance = memory;    // 3:設置instance指向剛分配的內存地址                                       
                       // 注意,此時對象還沒有被初始化!
ctorInstance(memory);  // 2:初始化對象

2和3之間雖然被重排序了,但這個重排序並不會違反intra-thread semantics。這個重排序在沒有改變單線程程序執行結果的前提下,可以提高程序的執行性能。

只要保證2排在4的前面,即使2和3之間重排序了,也不會違反intra-thread semantics。
 
 
 
錯誤結果
線程A的intra-thread semantics沒有改變,但A2和A3的重排序,將導致線程B在B1處判斷出instance不爲空,線程B接下來將訪問instance引用的對象。此時,線程B將會訪問到一個還未初始化的對象。
 
實現線程安全的延遲初始化的解決辦法
a. 不允許2和3重排序。
b. 允許2和3重排序,但不允許其他線程“看到”這個重排序。
 
a. 基於volatile的解決方案--即不允許2和3重排序
  • 只需要做一點小的修改(把instance聲明爲volatile型),就可以實現線程安全的延遲初始化。
  • 當聲明對象的引用爲volatile後,2和3之間的重排序,在多線程環境中將會被禁止。
private volatile static Instance instance;

Volatile的重排序規則:

  1. 當第二個操作是volatile寫時,不管第一個操作是什麼,都不能重排序。這個規則確保volatile寫之前的操作不會被編譯器重排序到volatile寫之
  2. 當第一個操作是volatile讀時,不管第二個操作是什麼,都不能重排序。這個規則確保volatile讀之後的操作不會被編譯器重排序到volatile讀之前。
  3. 當第一個操作是volatile寫,第二個操作是volatile讀時,不能重排序。
如何實現volatile的內存語義:JMM採取保守策略
下面是基於保守策略的JMM內存屏障插入策略:
  1. 在每個volatile寫操作的前面插入一個StoreStore屏障。
  2. 在每個volatile寫操作的後面插入一個StoreLoad屏障。
  3. 在每個volatile讀操作的後面插入一個LoadLoad屏障。
  4. 在每個volatile讀操作的後面插入一個LoadStore屏障。
上述內存屏障插入策略非常保守,但它可以保證在任意處理器平臺,任意的程序中都能得到正確的volatile內存語義。
 

b. 基於類初始化的解決方案--即允許2和3重排序,但不允許其他線程“看到”這個重排序

使用靜態內部類的方式
 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章