您好,登錄后才能下訂單哦!
這篇文章給大家介紹java高并發中什么是發布對象,內容非常詳細,感興趣的小伙伴們可以參考借鑒,希望對大家能有所幫助。
發布對象:是指使一個對象能夠被當前范圍之外的代碼所使用。
對象逸出:一種錯誤的發布。當一個對象還沒有構造完成時,就使它被其他線程所見。
我們經常需要發布一些對象例如通過類的非私有方法返回對象的引用,或者通過共有靜態變量發布對象。
簡單例子:
@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())); } }
輸出:
15:42:12.937 [main] INFO com.vincent.example.publish.UnsafePublish - [a, b, c] 15:42:12.942 [main] INFO com.vincent.example.publish.UnsafePublish - [d, b, c]
為什么要做這個例子?UnsafePublish通過getState()方法發布了一個屬性,在類的任何外部線程都可以訪問這個屬性,這樣發布對象是不安全的,因為我們無法確定其他線程會不會修改這個屬性,從而其他線程訪問這個屬性時,它的值是不確定的,這樣發布的對象就是線程不安全的。
先看一個例子:
@Slf4j @NotThreadSafe @NotRecommend public class Escape { private int thisCannBeEscape = 0; public Escape() { new InnerClass(); } private class InnerClass { public InnerClass() { log.info("{}",Escape.this.thisCannBeEscape); } } public static void main(String[] args) { new Escape(); } }
這個例子中定義了一個內部類InnerClass,這個內部類的中引用了外部類的一個引用,有可能在對象沒有正確被構造之前就被發布,有可能有不安全的因素在里面。
在靜態初始化函數中初始化一個對象引用
將對象的引用保存到volatile類型域或者AtomicReference對象中
將對象的引用保存到某個正確構造對象的final類型域中
將對象的引用保存到一個由鎖保護的域中
/** * 懶漢模式 * 單例的實例在第一次使用時進行創建 */ public class SingletonExample1 { //私有構造函數 private SingletonExample1(){ } //單例對象 private static SingletonExample1 instance = null; //靜態工廠方法 public static SingletonExample1 getInstance(){ if(instance == null){ instance = new SingletonExample1(); } return instance; } }
上面這段代碼在單線程的情況下沒有問題,在多線程的情況下會有問題,問題出現在代碼if(instance ==null){instance = new SingletonExample1();}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; } }
這樣的懶漢模式是線程安全,但是卻帶來了性能上的開銷。而這個開銷是我們不希望的。因此并不推薦這種寫法。
/** * 懶漢模式 -> 雙重同步鎖單例模式 * 單例的實例在第一次使用時進行創建 */ @NotThreadSafe @ThreadSafe @NotRecommend public class SingletonExample4 { //私有構造函數 private SingletonExample4(){ } //單例對象 private static SingletonExample4 instance = null; //正常的執行步驟 //1.memory = allcate() 分配對象的內存空間 //2.ctorInstance()初始化對象 //3.instance = memory 設置instance指向剛分配的內存 // JVM 和CPU優化,發生了指令重排 //1.memory = allcate() 分配對象的內存空間 //3.instance = memory 設置instance指向剛分配的內存 //2.ctorInstance()初始化對象 //靜態工廠方法 public static SingletonExample4 getInstance(){ if(instance == null){ //雙重檢測機制 synchronized (SingletonExample4.class) { //同步鎖 if (instance == null) { instance = new SingletonExample4(); } } } return instance; } }
正常的執行步驟 1.memory = allcate() 分配對象的內存空間 2.ctorInstance()初始化對象 3.instance = memory 設置instance指向剛分配的內存 上面的步驟在單線程的情況下沒有問題,而在多線程情況下JVM 和CPU優化,發生了指令重排 1.memory = allcate() 分配對象的內存空間 3.instance = memory 設置instance指向剛分配的內存 2.ctorInstance()初始化對象
發生指令重排導致雙重檢測機制線程不安全。因此可以限制不讓CPU發生指令重排。可以使用volatile關鍵字限制指令重排。
private static volatile SingletonExample5 instance = null;
這樣就可以實現線程安全了。
因此volatile關鍵字有兩個使用場景。1.狀態標示量。2.雙重檢測。這里就是一個雙重檢測的應用。
/** * 餓漢模式 * 單例的實例在類裝載時進行創建 */ @ThreadSafe public class SingletonExample2 { //私有構造函數 private SingletonExample2(){ } //單例對象 private static SingletonExample2 instance = new SingletonExample2(); //靜態工廠方法 public static SingletonExample2 getInstance(){ return instance; } }
如果單例類的構造函數中,沒有過多的操作處理,餓漢模式還可以接受。餓漢模式有什么不足呢?如果構造函數中存在過多的處理,會導致這個類在加載的過程中特別慢,因此可能存在性能問題,如果使用餓漢模式的話只進行類的加載卻沒有實際的調用的話,會造成資源的浪費。因此使用餓漢模式時一定要考慮兩個問題,1.私有構造函數中沒有太多的處理。2.這個類肯定會被使用,不會帶來資源的浪費。餓漢模式屬于線程安全的。
@ThreadSafe public class SingletonExample6 { //私有構造函數 private SingletonExample6(){ } static { instance = new SingletonExample6(); } //單例對象 private static SingletonExample6 instance = null; //靜態工廠方法 public static SingletonExample6 getInstance(){ return instance; } public static void main(String[] args) { System.out.println(getInstance()); System.out.println(getInstance()); } }
這時打印出來的值為null
需要把private static SingletonExample6 instance = null;代碼寫在static語句塊之前:
/** * 餓漢模式 * 單例的實例在類裝載時進行創建 */ @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()); System.out.println(getInstance()); } }
當我們在寫靜態屬性和靜態代碼塊時要注意順序。
/** * 餓漢模式 * 單例的實例在類裝載時進行創建 */ @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; } } }
枚舉模式相比于懶漢模式在安全性方面更容易保證,其次相比于餓漢模式可以在實際調用時才初始化,而在后續使用時也可以取到里面的值。不會造成資源浪費。
關于java高并發中什么是發布對象就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。