首頁  >  文章  >  Java  >  如何解決Java單例模式中的執行緒安全性問題?

如何解決Java單例模式中的執行緒安全性問題?

王林
王林轉載
2023-05-09 19:28:061427瀏覽

    一. 使用多執行緒需要考慮的因素

    #提高效率:使用多執行緒就是為了充分利用CPU資源,提高任務的效率
    線程安全:使用多線程最基本的就是保障線程安全問題

    ##所以我們在設計多線程程式碼的時候就必須在滿足執行緒安全的前提下盡可能的提高任務執行的效

    故:
    加上鎖定細粒度化:加鎖的程式碼少一點,讓其他程式碼可以並行的執行

    考慮執行緒安全性:

    沒有操作共享變數的程式碼沒有安全性問題

    對共享變數的讀,使用volatile修飾變數即可
    對共享變數的寫,使用synchronized加鎖

    二. 單例模式

    單例模式能保證某個類別在程式中只存在唯一一份實例,而不會建立出多個實例

    例如:DataSource(資料連接池),一個資料庫只需要一個連接池物件

    單例模式分為餓漢模式和懶漢模式

    1. 餓漢模式

    餓漢模式是在類別載入的時候就建立實例

    這種方式是滿足執行緒安全的(JVM內部使用了加鎖,即多個執行緒呼叫靜態方法,只有一個執行緒競爭到鎖定並且完成創建,只執行一次)

    實作程式碼:

    public class Singleton {
        private static Singleton instance = new Singleton();
        private Singleton(){
     
        }
        public static Singleton getInstance(){
            return instance;
        }
    }

    2.懶漢模式

    ##懶漢模式是在類別載入的時候不建立實例,第一次使用的時候才建立

    實作程式碼:
    public class Singleton {
        private static Singleton instance = null;
        private Singleton(){
     
        }
        public static Singleton getInstance(){
            if(instance == null){
                instance = new Singleton();
            }
            return instance;
        }
    }

    觀察上述程式碼,在單一執行緒下不存在線程安全性問題,但是在多線程環境下有安全性問題嗎?

    分析:

    當實例沒有被建立的時候,如果有多個執行緒都會呼叫getInstance方法,就可能建立多個實例,就存在執行緒安全性問題 

    但是實例一旦建立好,後面執行緒呼叫getInstance方法就不會出現執行緒安全性問題

    結果:

    執行緒安全問題出現在第一個建立實例的時候

    3. 懶漢模式(使用synchronized改進)

    我們使用sychronized修飾,????‍????️程式碼如下:
    public class Singleton {
        private static Singleton instance = null;
        private Singleton(){
     
        }
        public static synchronized Singleton getInstance(){
            if(instance == null){
                instance = new Singleton();
            }
            return instance;
        }
    }



    # #這樣實作線程安全存在什麼問題呢?
    解析:

    我們對方法使用synchronized修飾,也就是每次呼叫方法的時候都會競爭鎖,但建立實例只需要建立一次,也就是建立實例後,再呼叫該方法還需要競爭鎖定釋放鎖定

    結果:如何解決Java單例模式中的執行緒安全性問題?雖然滿足執行緒安全,但是效率低

    ##4.懶漢模式(使用雙重校驗鎖定)

    在上述程式碼的基礎上進行變更:

    使用雙重if判定,降低競爭鎖定頻率

    使用volatile修飾instance

    實作程式碼:

    public class Singleton {
        private static volatile Singleton instance = null;
        private Singleton(){
     
        }
        public static synchronized Singleton getInstance(){
            if(instance == null){ //外层的if判断:如果实例被创建直接return,不让线程再继续竞争锁
                //在没有创建实例时,多个线程已经进入if判断了
                //一个线程竞争到锁,其他线程阻塞等待
                synchronized (Singleton.class) {
                    //内层的if判断,目的是让竞争失败的锁如果再次竞争成功的话判断实例是否被创建,创建释放锁return,没有则创建
                    if(instance == null){ 
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }

    對雙重if的解析:

    ##外層的if判斷:

    實例只是被創建一次,當實例已經被創建好了就不要後續操作,直接return返回

    內層的if判斷:
    實例未被創建時,多個線程同時競爭鎖,只有一個線程競爭成功並創建實例,其他競爭失敗的線程就會阻塞等待,當第一線程釋放鎖定後,這些競爭失敗的線程就會繼續競爭,但是實例已經創建好了,所以需要再次進行if判斷 

    畫圖分析,如下:

    ##########三. volatile的原理 #########volatile保證了可見性,有序性,在Java層面看,volatile是無鎖定操作,多個執行緒對volatile修飾的變數進行讀取可以並發並行執行,和無鎖定執行效率差不多######volatile修飾的變數中, CPU使用了快取一致性協定來確保讀取的都是最新的主存資料 ######快取一致性:如果有別的執行緒修改了volatile修飾的變量,就會把CPU快取中的變數置為無效,要操作這個變數就要從主記憶體重新讀取######### 四.volatile的擴充問題(了解)#########如果說volatile不保證有序性,雙重校驗鎖的寫法是否有問題? #########關於new物件依序分為3條指令:#########(1) 分配物件的記憶體空間###(2) 實例化物件###( 3) 賦值給變數#########正常的執行順序為(1)(2)(3),JVM可能會最佳化進行重新排序後的順序為(1)(3)(2)# ##

    這個重排序的結果可能導致分配記憶體空間後,物件還沒有實例化完成,就完成了賦值
    在這個錯誤的賦值後,instance==null不成立,執行緒就會拿著未完成實例化的instance,使用它的屬性和方法就會出錯

    使用volatile保證有序性後:

    線程在new物件時不管(1 )(2)(3)是什麼順序,後續執行緒拿到的instance是已經實例化完成的
    CPU裡邊,基於volatile變數運算是有CPU等級的加鎖機制(它保證(1)(2) (3)全部執行完,寫回主存,再執行其他執行緒對該變數的操作)

    以上是如何解決Java單例模式中的執行緒安全性問題?的詳細內容。更多資訊請關注PHP中文網其他相關文章!

    陳述:
    本文轉載於:yisu.com。如有侵權,請聯絡admin@php.cn刪除