所謂的DCL 就是 Double Check Lock,即雙重鎖定檢查,在了解DCL在單例模式中如何應用之前,我們先了解一下單例模式,單例模式通常分為“餓漢”和“懶漢”,先從簡單入手
餓漢
所謂的“餓漢”是因為程式剛啟動時就創建了實體,通俗點說就是剛上菜,大家還沒有開始吃的時候就先自己吃一口,
public class Singleton {
private static final Singleton singleton = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return singleton;
}
}
第3行 通過一個私有構造方法限制了創建此類物件的途徑(反射忽略),這種方法很安全,但從某種程度上有點浪費資源,比方說從一開始就創建了Singleton實體,但很少去用它,這就造成了方法區資源的浪費,因此出現了另外一種單例模式,即懶漢單例模式
懶漢
之所以叫“懶漢”是因為只有真正叫它的時候,才會出現,不叫它它就不理,跟它沒關系,也就是說真正用到它的時候才去創建實體,并不是一開始就創建實體,如下代碼所示:
public class Singleton {
private static Singleton singleton = null;
private Singleton(){}
public static Singleton getInstance(){
if(null == singleton){
singleton = new Singleton();
}
return singleton;
}
}
看似很簡單的一段代碼,但存在一個問題,就是執行緒不安全的問題,例如,現在有1000個執行緒,都需要這一個Singleton的實體,驗證一下是否拿到同一個實體,代碼如下所示:
public class Singleton {
private static Singleton singleton = null;
private Singleton(){}
public static Singleton getInstance(){
if(null == singleton){
try {
Thread.sleep(1);//象征性的睡了1ms
} catch (InterruptedException e) {
e.printStackTrace();
}
singleton = new Singleton();
}
return singleton;
}
public static void main(String[] args) {
for (int i=0;i<1000;i++){
new Thread(()-> System.out.println(Singleton.getInstance().hashCode())).start();
}
}
}
部分運行結果,亂七八糟:
944436457
1638599176
710946821
67862359
為什么會這樣?第一個執行緒過來了,執行到第7行,睡了1ms,正在睡的同時第二個執行緒來了,第二個執行緒執行到第5行時,結果肯定為空,因此接下來將會有兩個執行緒各自創建一個物件,這必然會導致Singleton.getInstance().hashCode()結果不一致,可以通過給整個方法加上一把鎖改進如下:
改進1
public class Singleton {
private static Singleton singleton = null;
private Singleton(){}
public static synchronized Singleton getInstance(){
if(null == singleton){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
singleton = new Singleton();
}
return singleton;
}
public static void main(String[] args) {
for (int i=0;i<1000;i++){
new Thread(()-> System.out.println(Singleton.getInstance().hashCode())).start();
}
}
}
通過給getInstance()方法加上synchronized來解決執行緒一致性問題,結果分析雖然顯示所有實體的hashcode都一致,但是synchronized的粒度太大了,即鎖的臨界區太大了,有點影響效率,例如如果第4行和第5行之間有業務處理邏輯,不會涉及共享變數,那么每次對這部分業務邏輯加鎖必然會導致效率低下,為了解決粗粒度的問題,可以對代碼進一步改進:
改進2
public class Singleton {
private static Singleton singleton = null;
private Singleton(){}
public static Singleton getInstance(){
/*
一堆業務處理代碼
*/
if(null == singleton){
synchronized(Singleton.class){//鎖粒度變小
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
singleton = new Singleton();
}
}
return singleton;
}
public static void main(String[] args) {
for (int i=0;i<1000;i++){
new Thread(()-> System.out.println(Singleton.getInstance().hashCode())).start();
}
}
}
部分運行結果 :
391918859
391918859
391918859
1945023194
通過分析運行結果發現,雖然鎖的粒度變小了,但執行緒不安全了,為什么會這樣呢?因為有種情況,執行緒1執行完if判斷后還沒有拿到鎖的時候時間片用完了,此時執行緒2來了,執行if判斷時發現物件還是空的,繼續往下執行,很順利的拿到鎖了,因此執行緒2創建了一個物件,當執行緒2創建完之后釋放掉鎖,這時執行緒1激活了,順利的拿到鎖,又創建了一個物件,所以代碼還需要再一步的改進,
改進3
public class Singleton {
private static Singleton singleton = null;
private Singleton(){}
public static Singleton getInstance(){
/*
一堆業務處理代碼
*/
if(null == singleton){
synchronized(Singleton.class){//鎖粒度變小
if(null == singleton){//DCL
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
singleton = new Singleton();
}
}
}
return singleton;
}
public static void main(String[] args) {
for (int i=0;i<1000;i++){
new Thread(()-> System.out.println(Singleton.getInstance().hashCode())).start();
}
}
}
通過在第10行又加了一層if判斷,也就是所謂的Double Check Lock,也就是說即便拿到鎖了,也得去作一步判斷,如果這時判斷對像不為空,那么就不用再創建物件,直接回傳就可以了,很好的解決了“改進2”中的問題,但這里第8行是不是可以去了,我個人覺得都行,保留第8行的話,是為了提升效率,因為如果去了,每個執行緒過來就直接搶鎖,搶鎖本身就會影響效率,而if判斷就幾ns,且大部分執行緒是不需要搶鎖的,所以最好保留,
到這DCL 單例的原理就介紹完了,但是還是有一個問題,就是需要考慮指令重排序的問題,因此得加入volatile來禁止指令重排序,繼續分析代碼,為了分析方便這里將Singleton代碼簡化:
public class Singleton {
int a = 5;//考慮指令重排序的問題
}
singleton = new Singleton()的位元組碼如下:
0: new #2 // class com/reasearch/Singleton
3: dup
4: invokespecial #3 // Method com/reasearch/Singleton."<init>":()V
7: astore_1
先不管dup指令,這里補充一個知識點,創建物件的時候,先分配空間,類里面的變數先有一個默認值,等呼叫了構造方法后才給變數賦值,例如int a = 5 剛開始的時候 a = 0,位元組碼指令執行程序如下,
- new 分配空間,a=0
- invokespecial 構造方法 a=5
- astore_1將物件賦給singleton
這是理想的狀態,2和3語意和邏輯上沒有什么關聯,因此jvm可以允許這些指令亂序執行,即先執行3再執行2 ,回到改進3,假如執行緒1再執行第16行代碼時,指令的執行順序是1,3,2,當執行完3時,時間片用完了,此時a=0,也就是說初始化到一半時就掛起了,這時執行緒2 來了,第8行判斷,singleton肯定不為空,因此直接回傳一個Singleton的物件,但其實這個物件是一個問題物件,是一個半初始化的物件,即a=0 ,這就是指令重排序造成的,因此為了防止這種現象的發生加上關鍵字volatile就可以了,因而,最終DCL之單例模式的代碼完整版如下:
完整版
public class Singleton {
private volatile static Singleton singleton = null;//加上volatile
private Singleton(){}
public static Singleton getInstance(){
/*
一堆業務處理代碼
*/
if(null == singleton){
synchronized(Singleton.class){//鎖粒度變小
if(null == singleton){//DCL
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
singleton = new Singleton();
}
}
}
return singleton;
}
}
至此,可以告一段落了,相信很多小伙伴都會寫單例,但是了解其中的原理還是有一定的難度,大家一起加油!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/264138.html
標籤:java
上一篇:JUnit5學習之六:引數化測驗(Parameterized Tests)基礎
下一篇:二維碼之元宵節快樂
