1.發佈與逸出
1.1概念
發佈對象 : 使一個對象能夠被當前範圍之外的代碼所使用;
對象逸出 : 一種錯誤的發佈,當一個對象還沒有構造完成時,就使他被其他線程所見;
1.2代碼演示
/**
* 發佈對象
*/
@Slf4j
@NotThreadSafe
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()));
}
}
從結果可以看出這個類是不安全的,因爲我們無法確認別的線程是否對這個對象進行修改;
/**
* 對象逸出
*/
@Slf4j
@NotThreadSafe
@NotRecommend
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();
}
}
這個也是一個線程不安全的,這會導致發佈線程以外的其他線程會看到過期的值;
2.安全發佈對象的四種方法
① 在靜態初始化函數中初始化一個對象引用;
② 將對象的引用保存到volatile類型域或者AtomicReference對象中;
③ 將對象的引用保存到某個正確構造對象的final類型域中;
④ 將對象的引用保存到一個由鎖保護的域中;
典型的單例模式爲例:
/**
* 懶漢模式
* 單例實例在第一次使用時進行創建
*/
@NotThreadSafe
public class SingletonExample1 {
// 私有構造函數
private SingletonExample1() { }
// 單例對象
private static SingletonExample1 instance = null;
// 靜態的工廠方法
public static SingletonExample1 getInstance() {
if (instance == null) {
instance = new SingletonExample1();
}
return instance;
}
}
上邊的實現在單線程中沒有問題,因爲我們在對象創建之前進行了判斷,如果在多線程環境下就會出現問題,這個問題是由於多個線程同時獲取到了不同的初始化對象導致;
/**
* 餓漢模式
* 單例實例在類裝載時進行創建
*/
@ThreadSafe
public class SingletonExample2 {
// 私有構造函數
private SingletonExample2() { }
// 單例對象
private static SingletonExample2 instance = new SingletonExample2();
// 靜態的工廠方法
public static SingletonExample2 getInstance() {
return instance;
}
}
這個類是線程安全的,因爲我們使用了單例模式的餓漢式在類第一次被裝載的時候就會創建對象且因爲是靜態的又只會被創建一次,所以他是線程安全的;但是這個也是有缺點的,如果初始化的時候執行過多的操作會導致加載速度特別慢導致性能的問題,如果只進行資源的加載而沒有調用的話又會導致資源的浪費;
/**
* 懶漢模式
* 單例實例在第一次使用時進行創建
*/
@ThreadSafe
@NotRecommend
public class SingletonExample3 {
// 私有構造函數
private SingletonExample3() {
}
// 單例對象
private static SingletonExample3 instance = null;
// 靜態的工廠方法
public static synchronized SingletonExample3 getInstance() {
if (instance == null) {
instance = new SingletonExample3();
}
return instance;
}
}
獲取對象的方法經過synchronized的修飾就會出現在同一個時間段內只能有一個線程進行訪問,所以懶漢式也將會變成線程安全的;但是這樣的寫法我們並不推薦,因爲加了synchronized雖然保證了線程安全但是卻帶來了性能上的開銷;
/**
* 懶漢模式 -》 雙重同步鎖單例模式
* 單例實例在第一次使用時進行創建
*/
@NotThreadSafe
public class SingletonExample4 {
// 私有構造函數
private SingletonExample4() { }
// 單例對象
private static SingletonExample4 instance = null;
// 靜態的工廠方法
public static SingletonExample4 getInstance() {
if (instance == null) { // 雙重檢測機制 // B
synchronized (SingletonExample4.class) { // 同步鎖
if (instance == null) {
instance = new SingletonExample4(); // A - 3
}
}
}
return instance;
}
}
但是這個類也不是線程安全的,當我們執行到這一行代碼instance = new SingletonExample4();的時候;他會進行以下三步的操作:
1、memory = allocate() 分配對象的內存空間
2、ctorInstance() 初始化對象
3、instance = memory 設置instance指向剛分配的內存
在完成這三步以後我們的instance就指向實際分配的內存地址了;在單線程的情況是沒有什麼問題的但是在多線程情況下就會出現以下的情況:
JVM和cpu優化,發生了指令重排
1、memory = allocate() 分配對象的內存空間
3、instance = memory 設置instance指向剛分配的內存
2、ctorInstance() 初始化對象
當發生了指令重排序以後,這個類就會變成線程不安全的了;
/**
* 懶漢模式 -》 雙重同步鎖單例模式
* 單例實例在第一次使用時進行創建
*/
@ThreadSafe
public class SingletonExample5 {
// 私有構造函數
private SingletonExample5() {
}
// 1、memory = allocate() 分配對象的內存空間
// 2、ctorInstance() 初始化對象
// 3、instance = memory 設置instance指向剛分配的內存
// 單例對象 volatile + 雙重檢測機制 -> 禁止指令重排
private volatile static SingletonExample5 instance = null;
// 靜態的工廠方法
public static SingletonExample5 getInstance() {
if (instance == null) { // 雙重檢測機制 // B
synchronized (SingletonExample5.class) { // 同步鎖
if (instance == null) {
instance = new SingletonExample5(); // A - 3
}
}
}
return instance;
}
}
因爲前一個例子發生了指令重排序導致了線程不安全,那麼我們通過volatile關鍵字限制指令重排序這樣就會變成線程安全的了;這就是volatile的雙重檢測使用場景;關於懶漢模式我們就先分析到這裏,接下來我們看一下餓漢模式
/**
* 餓漢模式
* 單例實例在類裝載時進行創建
*/
@ThreadSafe
public class SingletonExample6 {
// 私有構造函數
private SingletonExample6() { }
// 單例對象
private static SingletonExample6 instance = null;
static {
instance = new SingletonExample6();
}
// 靜態的工廠方法
public static SingletonExample6 getInstance() {
return instance;
}
public static void main(String[] args) {
System.out.println(getInstance().hashCode());
System.out.println(getInstance().hashCode());
}
}
當我們在寫靜態域或者靜態代碼塊的時候一定要注意書寫順序否則會出現NullPointException;
/**
* 枚舉模式:最安全
*/
@ThreadSafe
@Recommend
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;
}
}
}
當我們通過枚舉來初始化這個對象的時候,它可以保證這個方法絕對只會被執行一次且是在這個類調用之前初始化的,因此這個類是線程絕對安全的,推薦使用這種方式,因爲這種方式比懶漢式更安全,比餓漢式更加節省資源;
總結:之前看書的時候一直不懂這個發佈也逸出。主要是聽着比較彆扭。通過結合單例模式的複習,學習到了安全發佈和逸出。