JAVA并发编程实战---第三章:对象的共享
在沒有同步的情況下,編譯器、處理器以及運行時等都可能對操作的執行順序進行一些意想不到的調整。在缺乏足夠同步的多線程程序中,要對內存操作的執行順序進行判斷幾乎無法得到正確的結果。
非原子的64位操作
當線程在沒有同步的情況下讀取變量時,可能會讀到一個失效值,但至少這個值是由之前的某個線程設置,而不是一個隨機值。這種安全性保證也被稱為最低安全性。
Java內存模型要求:變量的讀取操作和寫入操作都必須是原子操作,但對于非Volatile類型的long和Double變量,JVM允許將64的讀操作或寫操作,分解成兩個32位的操作。因此,及時不考慮失效數據問題,在多線程中使用共享且可變的long或double等類型的變量也是不安全的,除非使用Volatile關鍵字或鎖保護起來。
加鎖的含義不僅僅局限于互斥行為,還包括內存可見性。為了確保所有線程都能看到共享變量的最新值,所有執行讀操作或寫操作的線程必須在同一個鎖上同步。
Volatile
Volatile變量用來確保將變量的更新操作通知到其他線程。當把變量申明為Volatile類型后,編譯器與運行時都會注意到這個變量是共享變量,因此不會講該變量上的操作與其他內存操作一起重排序。Volatile變量不會被緩存在寄存器或者其他處理器不可見的地方,一次讀取Volatile類型的變量總是會返回最新寫入的值。
加鎖機制既可以確??梢娦杂挚梢源_保原子性,而Volatile變量只能確??梢娦浴?/span>
當且僅當滿足以下所有條件時,才應該使用Volatile變量:
- 對變量的寫入操作不依賴與變量的當前值,或者能確保只有單個線程更新變量的值。
- 該變量不會與其他狀態變量一起納入不變性條件中。
- 在訪問變量時不需要加鎖。
對象的發布與逸出
發布:“發布”一個對象是指,使對象能夠在當前作用域之外的代碼中使用。
例如:將一個指向該對象的引用保存到其他代碼可以訪問的地方,或者在某一個非私有的方法中返回該對象的引用,或者將引用傳遞到其他類的方法中。
代碼示范:將一個指向該對象的引用,保存到其他代碼可以訪問的地方。
// 保存在一個共有的靜態變量中 public static Set<Secret> knowSecrets; public void initialize(){ knowSecrets = new HashSet<Secret>(); }逸出:“逸出”是指,某個不應該發布的對象被發布出去。
當發布某個對象時,可能會間接地發布其他對象。例如上述代碼,若將一個 Secret 對象添加到 knownSecret 中,那么會發布這個?Secret 對象;因為任何代碼都可以遍歷這個集合,并獲得對??Secret 對象的引用。同樣,如果從非私有方法中返回一個引用,則會發布返回對象。
代碼示范:某一個非私有的方法中返回私有對象的引用。
class UnsafeStates{private String[] states = new String[]{"AK","AL"};public String[] getStates(){ return states; } }上述代碼中,數組 states 已經逸出了它所在的作用域,因為這個本應是私有的變量,已經被發布了。
逸出范圍:當一個對象A被發布時,在A的非私有域中引用的所有對象同樣會被發布。也就是,一個已經發布的對象A,能夠通過非私有的變量引用和方法調用到達其他的對象,那么所能夠到達的對象,均會跟隨A一起發布。
此處給出一個定義:“外部方法”。假如有一個類C,對于C來說,“外部方法”是指行為不完全由C來規定的方法,包括其他類中定義的方法以及類C中可以被改寫的方法(既不是[private]方法,也不是[final]方法)。
當把一個對象傳遞給外部方法時,則該對象就會面臨一定的危險,因為你不知道外部方法會對該對象做些什么,因此我們需要使用封裝。封裝能夠使得對程序的正確性進行分析變得可能,并使得無意中破壞設計約束條件變得困難。
工廠方法避免this引用在構造方法中逸出:
首先了解 this 引用是如何在構造方法中逸出的。先看一段代碼:發布一個內部類的實例。
public class ThisEscape{public ThisEscape(EventSource source){source.registerListener(new EventListener(){ public void onEvent(Event e){ doSomething(e); } }); } }上述代碼中,當 ThisEscape 發布 EventListener 時,也隱含發布了 ThisEscape 實例本身,因為這個內部類的實例中包含了對 ThisEscapse 實例的隱含引用。只要其他線程在ThisEscape未構造之前(構造返回狀態)調用這個類,那么this就會被新建線程共享并識別它(線程溢出)。
下面的代碼對上面的示例進行解釋:
public class ThisEscape {int i = 100; public ThisEscape(int j){ new Thread(new Runnable() { @Override public void run() { System.out.println(i + j); } }).start(); } public static void main(String[] args) { ThisEscape thisE = new ThisEscape(100); } }上述代碼給出了逸出的一個特殊示例,即this引用在構造方法中逸出。當內部的new Thread發布時,在外部封裝的ThisEscape實例也逸出了,于是可以取到 i 值與 j 進行計算。因此當從對象的構造方法中發布對象時,只是發布了一個尚未構造完成的對象。
在構造方法中使用 this 引用逸出的常見錯誤:在構造方法中啟動一個線程;在構造方法中調用一個可改寫的實例方法。
如果想在構造方法中注冊一個事件監聽器或啟動線程,那么可以使用一個私有的構造方法和一個公共的工廠方法,從而避免不必要的構造過程。如以下程序所示:
public class SafeListener{private final EventListener listener; private SafeListener(){ listener = new EventListener(){ public void onEvent(Event e){ doSomething(e); } } } public static SafeListener newInstance(EventSource source){ SafeListener safe = new SafeListener(); source.registerListener(safe.listener); return safe; } }上面的代碼為在構造方法中注冊一個事件監聽器,新建的線程無法在構造方法之前共享和識別 safe。
轉載于:https://www.cnblogs.com/wxgblogs/p/5462869.html
《新程序員》:云原生和全面數字化實踐50位技術專家共同創作,文字、視頻、音頻交互閱讀總結
以上是生活随笔為你收集整理的JAVA并发编程实战---第三章:对象的共享的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: perl5 第九章 关联数组/哈希表
- 下一篇: Vysor:安卓手机放到电脑上用