程式碼例項解讀如何安全釋出物件

語言: CN / TW / HK
摘要:在高併發環境下如何安全的釋出物件例項。

本文分享自華為雲社群《【高併發】如何安全的釋出物件(含各種單例程式碼分析)》,作者:冰 河。

今天,為大家帶來一篇有技術含量的文章,那就是在高併發環境下如何安全的釋出物件例項。

釋出物件:使一個物件能夠被當前範圍之外的程式碼所使用
物件溢位:是一種錯誤的釋出,當一個物件還沒有構造完成時,就使它被其他執行緒所見

不安全的釋出示例程式碼:

package io.binghe.concurrency.example.publish;
import lombok.extern.slf4j.Slf4j;
import java.util.Arrays;
@Slf4j
public class UnsafePublish {
 private String[] states = {"a", "b", "c"};
 public String[] getStates(){
 return states;
    }
 public static void main(String[] args){
 UnsafePublish unsafePublish = new UnsafePublish();
 log.info("{}", Arrays.toString(unsafePublish.getStates()));
 unsafePublish.getStates()[0] = "d";
 log.info("{}", Arrays.toString(unsafePublish.getStates()));
    }
}

其中,每個執行緒都能獲取到UnsafePublish類的私有成員變數states,並修改states陣列的元素值,造成其他執行緒獲取的states元素值不確定。

物件溢位示例程式碼:

package io.binghe.concurrency.example.publish;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class Escape {
 private int thisCanBeEscape = 0;
 public Escape(){
 new InnerClass();
    }
 private class InnerClass{
 public InnerClass(){
 log.info("{}", Escape.this.thisCanBeEscape);
        }
    }
 public static void main(String[] args){
 new Escape();
    }
}

其中,內部類InnerClass的構造方法中包含了對封裝例項Escape的隱含的引用(體現在InnerClass的構造方法中引用了Escape.this),在物件沒有被正確構造完成之前,就會被髮布,有可能存在不安全的因素。

一個導致this在構造期間溢位的錯誤:在建構函式中,啟動一個執行緒,無論是隱式的啟動還是顯式的啟動,都會造成this引用的溢位(因為新執行緒總是在所屬物件構造完畢之前就已經看到this引用了)。所以,如果要在建構函式中建立執行緒,則不要在建構函式中啟動執行緒,可以使用一個專有的start()方法或者一個初始化方法,來統一啟動執行緒,可以採用工廠方法和私有建構函式來完成物件的建立和監聽器的註冊,之後統一啟動執行緒,來避免溢位。

注意:在物件未構造完成之前,不可以將其釋出

如何安全的釋出物件:

(1)在靜態初始化函式中初始化一個物件引用
(2)將物件的引用儲存到volatile型別域或者AtomicReference物件中
(3)將物件的引用儲存到某個正確構造物件的final型別域中
(4)將物件的引用儲存到一個由鎖保護的域中

接下來,看幾個單例物件的示例程式碼,其中有些程式碼是執行緒安全的,有些則不是執行緒安全的,需要大家細細品味,這些程式碼也是冰河本人在高併發環境下測試驗證過的。

程式碼一:SingletonExample1

這個類是懶漢模式,並且是執行緒不安全的

package io.binghe.concurrency.example.singleton;
/**
 * @author binghe
 * @version 1.0.0
 * @description 懶漢模式,單例例項在第一次使用的時候進行建立,這個類是執行緒不安全的
 */
public class SingletonExample1 {
 private SingletonExample1(){}
 private static SingletonExample1 instance = null;
 public static SingletonExample1 getInstance(){
 //多個執行緒同時呼叫,可能會建立多個物件
 if (instance == null){
            instance = new SingletonExample1();
        }
 return instance;
    }
}

程式碼二:SingletonExample2

餓漢模式,單例例項在類裝載的時候進行建立,是執行緒安全的

package io.binghe.concurrency.example.singleton;
/**
 * @author binghe
 * @version 1.0.0
 * @description 餓漢模式,單例例項在類裝載的時候進行建立,是執行緒安全的
 */
public class SingletonExample2 {
 private SingletonExample2(){}
 private static SingletonExample2 instance = new SingletonExample2();
 public static SingletonExample2 getInstance(){
 return instance;
    }
}

程式碼三:SingletonExample3

懶漢模式,單例例項在第一次使用的時候進行建立,這個類是執行緒安全的,但是這個寫法不推薦

package io.binghe.concurrency.example.singleton;
/**
 * @author binghe
 * @version 1.0.0
 * @description 懶漢模式,單例例項在第一次使用的時候進行建立,這個類是執行緒安全的,但是這個寫法不推薦
 */
public class SingletonExample3 {
 private SingletonExample3(){}
 private static SingletonExample3 instance = null;
 public static synchronized SingletonExample3 getInstance(){
 if (instance == null){
            instance = new SingletonExample3();
        }
 return instance;
    }
}

程式碼四:SingletonExample4

懶漢模式(雙重鎖同步鎖單例模式),單例例項在第一次使用的時候進行建立,但是,這個類不是執行緒安全的!!!!!

package io.binghe.concurrency.example.singleton;
/**
 * @author binghe
 * @version 1.0.0
 * @description 懶漢模式(雙重鎖同步鎖單例模式)
 *              單例例項在第一次使用的時候進行建立,這個類不是執行緒安全的
 */
public class SingletonExample4 {
 private SingletonExample4(){}
 private static SingletonExample4 instance = null;
 //執行緒不安全
 //當執行instance = new SingletonExample4();這行程式碼時,CPU會執行如下指令:
 //1.memory = allocate() 分配物件的記憶體空間
 //2.ctorInstance() 初始化物件
 //3.instance = memory 設定instance指向剛分配的記憶體
 //單純執行以上三步沒啥問題,但是在多執行緒情況下,可能會發生指令重排序。
 // 指令重排序對單執行緒沒有影響,單執行緒下CPU可以按照順序執行以上三個步驟,但是在多執行緒下,如果發生了指令重排序,則會打亂上面的三個步驟。
 //如果發生了JVM和CPU優化,發生重排序時,可能會按照下面的順序執行:
 //1.memory = allocate() 分配物件的記憶體空間
 //3.instance = memory 設定instance指向剛分配的記憶體
 //2.ctorInstance() 初始化物件
 //假設目前有兩個執行緒A和B同時執行getInstance()方法,A執行緒執行到instance = new SingletonExample4(); B執行緒剛執行到第一個 if (instance == null){處,
 //如果按照1.3.2的順序,假設執行緒A執行到3.instance = memory 設定instance指向剛分配的記憶體,此時,執行緒B判斷instance已經有值,就會直接return instance;
 //而實際上,執行緒A還未執行2.ctorInstance() 初始化物件,也就是說執行緒B拿到的instance物件還未進行初始化,這個未初始化的instance物件一旦被執行緒B使用,就會出現問題。
 public static SingletonExample4 getInstance(){
 if (instance == null){
 synchronized (SingletonExample4.class){
 if(instance == null){
                    instance = new SingletonExample4();
                }
            }
        }
 return instance;
    }
}

執行緒不安全分析如下:

當執行instance = new SingletonExample4();這行程式碼時,CPU會執行如下指令:

1.memory = allocate() 分配物件的記憶體空間
2.ctorInstance() 初始化物件
3.instance = memory 設定instance指向剛分配的記憶體

單純執行以上三步沒啥問題,但是在多執行緒情況下,可能會發生指令重排序。

指令重排序對單執行緒沒有影響,單執行緒下CPU可以按照順序執行以上三個步驟,但是在多執行緒下,如果發生了指令重排序,則會打亂上面的三個步驟。

如果發生了JVM和CPU優化,發生重排序時,可能會按照下面的順序執行:

1.memory = allocate() 分配物件的記憶體空間
2.instance = memory 設定instance指向剛分配的記憶體
3.ctorInstance() 初始化物件

假設目前有兩個執行緒A和B同時執行getInstance()方法,A執行緒執行到instance = new SingletonExample4(); B執行緒剛執行到第一個 if (instance == null){處,如果按照1.3.2的順序,假設執行緒A執行到3.instance = memory 設定instance指向剛分配的記憶體,此時,執行緒B判斷instance已經有值,就會直接return instance;而實際上,執行緒A還未執行2.ctorInstance() 初始化物件,也就是說執行緒B拿到的instance物件還未進行初始化,這個未初始化的instance物件一旦被執行緒B使用,就會出現問題。

程式碼五:SingletonExample5

懶漢模式(雙重鎖同步鎖單例模式)單例例項在第一次使用的時候進行建立,這個類是執行緒安全的,使用的是 volatile + 雙重檢測機制來禁止指令重排達到執行緒安全

package io.binghe.concurrency.example.singleton;
/**
 * @author binghe
 * @version 1.0.0
 * @description 懶漢模式(雙重鎖同步鎖單例模式)
 *              單例例項在第一次使用的時候進行建立,這個類是執行緒安全的
 */
public class SingletonExample5 {
 private SingletonExample5(){}
 //單例物件  volatile + 雙重檢測機制來禁止指令重排
 private volatile static SingletonExample5 instance = null;
 public static SingletonExample5 getInstance(){
 if (instance == null){
 synchronized (SingletonExample5.class){
 if(instance == null){
                    instance = new SingletonExample5();
                }
            }
        }
 return instance;
    }
}

程式碼六:SingletonExample6

餓漢模式,單例例項在類裝載的時候(使用靜態程式碼塊)進行建立,是執行緒安全的

package io.binghe.concurrency.example.singleton;
/**
 * @author binghe
 * @version 1.0.0
 * @description 餓漢模式,單例例項在類裝載的時候進行建立,是執行緒安全的
 */
public class SingletonExample6 {
 private SingletonExample6(){}
 private static SingletonExample6 instance = null;
 static {
        instance = new SingletonExample6();
    }
 public static SingletonExample6 getInstance(){
 return instance;
    }
}

程式碼七:SingletonExample7

列舉方式進行例項化,是執行緒安全的,此種方式也是執行緒最安全的

package io.binghe.concurrency.example.singleton;
/**
 * @author binghe
 * @version 1.0.0
 * @description 列舉方式進行例項化,是執行緒安全的,此種方式也是執行緒最安全的
 */
public class SingletonExample7 {
 private SingletonExample7(){}
 public static SingletonExample7 getInstance(){
 return Singleton.INSTANCE.getInstance();
    }
 private enum Singleton{
        INSTANCE;
 private SingletonExample7 singleton;
 //JVM保證這個方法絕對只調用一次
 Singleton(){
            singleton = new SingletonExample7();
        }
 public SingletonExample7 getInstance(){
 return singleton;
        }
    }
}

 

點選關注,第一時間瞭解華為雲新鮮技術~