1??發(fā)布與逸出
① 概念
發(fā)布對象 : 使一個對象能夠被當(dāng)前范圍之外的代碼所使用;
對象逸出 : 一種錯誤的發(fā)布,當(dāng)一個對象還沒有構(gòu)造完成時,就使他被其他線程所見;
② 代碼演示
/**
* 發(fā)布對象
*/
@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()));
}
}
從結(jié)果可以看出這個類是不安全的,因?yàn)槲覀儫o法確認(rèn)別的線程是否對這個對象進(jìn)行修改;
/**
* 對象逸出
*/
@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();
}
}
這個也是一個線程不安全的,這會導(dǎo)致發(fā)布線程以外的其他線程會看到過期的值;
2??安全發(fā)布對象的四種方法
① 在靜態(tài)初始化函數(shù)中初始化一個對象引用;
② 將對象的引用保存到volatile類型域或者AtomicReference對象中;
③ 將對象的引用保存到某個正確構(gòu)造對象的final類型域中;
④ 將對象的引用保存到一個由鎖保護(hù)的域中;
/**
* 懶漢模式
* 單例實(shí)例在第一次使用時進(jìn)行創(chuàng)建
*/
@NotThreadSafe
public class SingletonExample1 {
// 私有構(gòu)造函數(shù)
private SingletonExample1() { }
// 單例對象
private static SingletonExample1 instance = null;
// 靜態(tài)的工廠方法
public static SingletonExample1 getInstance() {
if (instance == null) {
instance = new SingletonExample1();
}
return instance;
}
}
上邊的實(shí)現(xiàn)在單線程中沒有問題,因?yàn)槲覀冊趯ο髣?chuàng)建之前進(jìn)行了判斷,如果在多線程環(huán)境下就會出現(xiàn)問題,這個問題是由于多個線程同時獲取到了不同的初始化對象導(dǎo)致;
/**
* 餓漢模式
* 單例實(shí)例在類裝載時進(jìn)行創(chuàng)建
*/
@ThreadSafe
public class SingletonExample2 {
// 私有構(gòu)造函數(shù)
private SingletonExample2() { }
// 單例對象
private static SingletonExample2 instance = new SingletonExample2();
// 靜態(tài)的工廠方法
public static SingletonExample2 getInstance() {
return instance;
}
}
這個類是線程安全的,因?yàn)槲覀兪褂昧藛卫J降酿I漢式在類第一次被裝載的時候就會創(chuàng)建對象且因?yàn)槭庆o態(tài)的又只會被創(chuàng)建一次,所以他是線程安全的;但是這個也是有缺點(diǎn)的,如果初始化的時候執(zhí)行過多的操作會導(dǎo)致加載速度特別慢導(dǎo)致性能的問題,如果只進(jìn)行資源的加載而沒有調(diào)用的話又會導(dǎo)致資源的浪費(fèi);
/**
* 懶漢模式
* 單例實(shí)例在第一次使用時進(jìn)行創(chuàng)建
*/
@ThreadSafe
@NotRecommend
public class SingletonExample3 {
// 私有構(gòu)造函數(shù)
private SingletonExample3() {
}
// 單例對象
private static SingletonExample3 instance = null;
// 靜態(tài)的工廠方法
public static synchronized SingletonExample3 getInstance() {
if (instance == null) {
instance = new SingletonExample3();
}
return instance;
}
}
獲取對象的方法經(jīng)過synchronized的修飾就會出現(xiàn)在同一個時間段內(nèi)只能有一個線程進(jìn)行訪問,所以懶漢式也將會變成線程安全的;但是這樣的寫法我們并不推薦,因?yàn)榧恿藄ynchronized雖然保證了線程安全但是卻帶來了性能上的開銷;
/**
* 懶漢模式 -》 雙重同步鎖單例模式
* 單例實(shí)例在第一次使用時進(jìn)行創(chuàng)建
*/
@NotThreadSafe
public class SingletonExample4 {
// 私有構(gòu)造函數(shù)
private SingletonExample4() { }
// 單例對象
private static SingletonExample4 instance = null;
// 靜態(tài)的工廠方法
public static SingletonExample4 getInstance() {
if (instance == null) { // 雙重檢測機(jī)制 // B
synchronized (SingletonExample4.class) { // 同步鎖
if (instance == null) {
instance = new SingletonExample4(); // A - 3
}
}
}
return instance;
}
}
但是這個類也不是線程安全的,當(dāng)我們執(zhí)行到這一行代碼instance = new SingletonExample4();的時候;他會進(jìn)行以下三步的操作:
1粉寞、memory = allocate() 分配對象的內(nèi)存空間
2、ctorInstance() 初始化對象
3栏豺、instance = memory 設(shè)置instance指向剛分配的內(nèi)存
在完成這三步以后我們的instance就只想實(shí)際分配的內(nèi)存地址了;在單線程的情況是沒有什么問題的但是在多線程情況下就會出現(xiàn)以下的情況:
JVM和cpu優(yōu)化英染,發(fā)生了指令重排
1秃诵、memory = allocate() 分配對象的內(nèi)存空間
3、instance = memory 設(shè)置instance指向剛分配的內(nèi)存
2、ctorInstance() 初始化對象
當(dāng)發(fā)生了指令重排序以后,這個類就會變成線程不安全的了;
/**
* 懶漢模式 -》 雙重同步鎖單例模式
* 單例實(shí)例在第一次使用時進(jìn)行創(chuàng)建
*/
@ThreadSafe
public class SingletonExample5 {
// 私有構(gòu)造函數(shù)
private SingletonExample5() {
}
// 1妻柒、memory = allocate() 分配對象的內(nèi)存空間
// 2、ctorInstance() 初始化對象
// 3猫缭、instance = memory 設(shè)置instance指向剛分配的內(nèi)存
// 單例對象 volatile + 雙重檢測機(jī)制 -> 禁止指令重排
private volatile static SingletonExample5 instance = null;
// 靜態(tài)的工廠方法
public static SingletonExample5 getInstance() {
if (instance == null) { // 雙重檢測機(jī)制 // B
synchronized (SingletonExample5.class) { // 同步鎖
if (instance == null) {
instance = new SingletonExample5(); // A - 3
}
}
}
return instance;
}
}
因?yàn)榍耙粋€例子發(fā)生了指令重排序?qū)е铝司€程不安全,那么我們通過volatile關(guān)鍵字限制指令重排序這樣就會變成線程安全的了;這就是volatile的雙重檢測使用場景;關(guān)于懶漢模式我們就先分析到這里,接下來我們看一下惡漢模式
/**
* 餓漢模式
* 單例實(shí)例在類裝載時進(jìn)行創(chuàng)建
*/
@ThreadSafe
public class SingletonExample6 {
// 私有構(gòu)造函數(shù)
private SingletonExample6() { }
// 單例對象
private static SingletonExample6 instance = null;
static {
instance = new SingletonExample6();
}
// 靜態(tài)的工廠方法
public static SingletonExample6 getInstance() {
return instance;
}
public static void main(String[] args) {
System.out.println(getInstance().hashCode());
System.out.println(getInstance().hashCode());
}
}
當(dāng)我們在寫靜態(tài)域或者靜態(tài)代碼塊的時候一定要注意書寫順序否則會出現(xiàn)NPE;
/**
* 枚舉模式:最安全
*/
@ThreadSafe
@Recommend
public class SingletonExample7 {
// 私有構(gòu)造函數(shù)
private SingletonExample7() { }
public static SingletonExample7 getInstance() {
return Singleton.INSTANCE.getInstance();
}
private enum Singleton {
INSTANCE;
private SingletonExample7 singleton;
// JVM保證這個方法絕對只調(diào)用一次
Singleton() {
singleton = new SingletonExample7();
}
public SingletonExample7 getInstance() {
return singleton;
}
}
}
當(dāng)我們通過枚舉來初始化這個對象的時候,它可以保證這個方法絕對只會被執(zhí)行一次且是在這個類調(diào)用之前初始化的,因此這個類是線程絕對安全的,推薦使用這種方式,因?yàn)檫@種方式比懶漢式更安全,比餓漢式更加節(jié)省資源;