單例的目的是為了保證運行時Singleton類只有唯一的一個實例,用于一些較大開銷的操作。
餓漢式(沒有線程安全問題):
‘
由于使用static關鍵字進行了修飾,只能獲取到一個對象,從而達到了單例,并且在Singleton類初始化的時候就創建了對象,加載到了內存。
問題:在沒有使用這個對象的情況下就加載到內存是一種很大的浪費。
針對這種情況,有一種新的思想提出——延遲加載,也就是所謂的懶漢式。
懶漢式(存在線程安全問題):
?
這種方法在調用Singleton.getInstance()時才會創建對象,起到了延遲加載的作用。
問題:這樣的寫法在多個線程同時運行時,很有可能會產生多個實例對象,導致線程安全問題。
使用同步的方法解決這個問題,加上synchronized關鍵字,代碼如下:
使用同步的代價是會在一定程度上降低程序的并發度,并且鎖定整個方法很消耗資源,原本采用延遲加載是為了節省資源,
所以,降低鎖的細粒度,代碼如下:
但是這樣的寫法線程還是不安全,因為兩個線程可以同時進入if語句,線程A實例化對象返回之后,線程B不用經過判斷
能再實例化對象,并且返回另一個對象。為了解決這個問題,引入了臭名昭著的雙重鎖機制:
上面的代碼看似解決了線程安全問題,也起到了延遲加載的作用,但是雙重鎖機制是沒有辦法工作的,有一篇文章解釋的非常
深刻:http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
在參考了一些資料,我認為雙重鎖機制之所以不能正常運行是因為,在new對象的時候,是有三個步驟的:分配內存空間,
初始化對象,然后將內存地址賦值給變量;在這么三個步驟中,極有可能會在操作上進行重排序,在重排序的情況下,還沒有初始化
對象,先將內存地址賦值給了變量(這種情況是可能存在的),當線程B進入時,發現變量不為null,就會直接返回這個實例,然而此時
可能拿到的是還沒有初始化完成的對象。所以雙重鎖機制是不提倡使用的。
在http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html#dcl文章中有提出,在新的內存模型下,實例字段使用volatile可以解
決雙重鎖檢查的問題,因為在新的內存模型下,volatile禁止了一些重排序,但是,同時,使用volatile的性能開銷也有所上升。
所以又提出一種新的模式——Initialization on Demand Holder.?這種方法使用內部類來做到延遲加載對象,在初始化這個內部類的時候,
JLS(Java Language Sepcification)會保證這個類的線程安全。代碼如下:
?
?