這里我們采用同步代碼塊來達到線程安全
我們給 getInstance() 方法創建實例時加了一把鎖 synchronzed,這樣會導致這個方法的并發為1,相當于串行操作,如果這個單例在實際項目中會頻繁被調用,那就會頻繁加鎖,釋放鎖,會有性能瓶頸,不推薦此種方式。
分析:上面的例子我們可以看到,synchronized 其實將方法內部的所有語句都已經包括了,每一個進來的線程都要單獨進入同步代碼塊,判斷實例是否存在,這就造成了性能的浪費。那么我們可以想到,其實在第一次已經創建了實例的情況下,后面再獲取實例的時候,可不可以不進入這個同步代碼塊?
以上的真的完美解決了單例模式嗎?其實并沒有,請看下面:
我們知道編譯就是將源代碼翻譯成機械碼的過程,而Java虛擬機的目標代碼不是本地機器碼,而是虛擬機代碼。編譯原理里面有個過程是編譯優化,就是指在不改變原來語義的情況下,通過調整語句的順序,來讓程序運行的更快,這個過程稱為 reorder。
JVM 只是一個標準,它并沒有規定有關編譯器優化的內容,也就是說,JVM可以自由的實現編譯器優化。
那么我們來再來考慮一下,創建一個變量需要哪些步驟?
①、申請一塊內存,調用構造方法進行初始化
②、分配一個指針指向該內存
而這兩步誰先誰后呢?也就是存在這樣一種情況:先開辟一塊內存,然后分配一個指針指向該內存,最后調用構造方法進行初始化。
那么針對單例模式的設計,就會存在這樣一個問題:線程 A 開始創建 Singleton 的實例,此時線程 B已經調用了 getInstance的()方法,首先判斷 instance 是否為 null。而我們上面說的那種模型, A 已經把 instance 指向了那塊內存,只是還沒來得及調用構造方法進行初始化,因此 B 檢測到 instance 不為 null,于是直接把 instance 返回了。那么問題出現了:盡管 instance 不為 null,但是 A 并沒有構造完成,就像一套房子已經給了你鑰匙,但是里面還沒有裝修,你并不能住進去。
解決方案:使用 volatile 關鍵字修飾 instance
我們知道在當前的Java內存模型下,線程可以把變量保存在本地內存(比如機器的寄存器)中,而不是直接在主存中進行讀寫。這就可能造成一個線程在主存中修改了一個變量的值,而另外一個線程還繼續使用它在寄存器中的變量值的拷貝,造成數據的不一致。
volatile修飾的成員變量在每次被線程訪問時,都強迫從共享內存中重讀該成員變量的值。而且,當成員變量發生變化時,強迫線程將變化值回寫到共享內存。這樣在任何時刻,兩個不同的線程總是看到某個成員變量的同一個值。
到此我們完美的解決了單例模式的問題。但是 volatile 關鍵字是 JDK1.5 才有的,也就是 JDK1.5 之前是不能這樣用的
上面我們說要加關鍵字 volatile ,禁止指令重排,防止單例對象new 出來后,并且賦值給 singleton,但是還沒來得及初始化這個問題。
現在高版本的 Java(JDK9) 已經在 JDK 內部實現中解決了這個問題,把對象的 new 操作和初始化操作設計為 原子操作。
相關參考鏈接:
https://shipilev.net/blog/2014/safe-public-construction
https://chriswhocodes.com/vm-options-explorer.html
通過Java枚舉類的自身特性,保證實例創建的線程安全和唯一性。
說了那么多,那么單例模式在實際項目中有啥用呢?
還是根據其核心概念,某個數據在系統中只能存在一份,就可以設計為單例。
1、windows 系統的回收站,我們能在任何盤符刪除數據,但是最后都是到了回收站中
2、網站的計數器,不采用單例模式,很難實現同步
3、數據庫連接池,可以節省打開或關閉數據庫連接所引起的效率損耗,用單例模式來維護,可以大大降低這種損耗。當然對于海量數據系統,會存在多個數據庫連接池,比如一個能夠快速執行SQL的連接池,還有一個是慢SQL,如果都放在一個池里面,會導致慢SQL執行的時候,長時間占用數據庫連接資源,導致其他SQL請求無法響應。
4、系統的配置信息類,通常只存在一個。