有點類似于數據庫事務中的臟讀,不可重復讀問題,不同的事務就相當于不同的線程,如果沒有設置事務的隔離級別,即線程之間沒有使用同步機制,這時候 A事務 對數據庫的 修改操作由于事務沒有提交,所以B事務是看不到A事務修改后的數據的,即發生了臟讀。類似的在多線程沒有使用同步的情況下,A線程對變量修改,B線程讀取變量,在沒有同步的情況下,AB的執行順序是不能保證的。示例代碼如下,輸出結果可能為0或者沒有輸出。
package net.jcip.examples;/** * NoVisibility * <p/> * Sharing variables without synchronization * * @author Brian Goetz and Tim Peierls */public class NoVisibility { PRivate static boolean ready; private static int number; private static class ReaderThread extends Thread { public void run() { while (!ready) Thread.yield(); System.out.println(number); } } public static void main(String[] args) { new ReaderThread().start(); number = 42; ready = true; }}在沒有同步的情況下,編譯器、處理器以及運行時等都可能對操作的執行順序,進行一些意想不到的調整,在缺乏足夠同步的多線程中,想要對內存操作的執行順序進行判斷,幾乎無法得出正確的結論。
NoVisibility中當讀線程查看ready變量時,可能會得到一個已經失效的值。
對于 非volatile類型的64位數值變量,JVM允許將64位的讀操作或寫操作分解為兩個32位操作,當讀取一個非volatile類型的long變量時,如果對該變量的讀操作和寫操作在不同的線程中執行,那么很可能會讀取到某個值的高32位和另一個值的低32位的組合結果。這樣就不僅僅是內存可見性問題了。
加鎖的含義不僅僅局限于線程間的互斥行為,還包括內存可見性。
當把變量聲明為volatile變量之后,編譯與運行時,JVM都會注意到這個變量時共享的,時內存可見的,在該變量上的操作,不會與其他的內存操作一起重排序,volatile不會被緩存在寄存器或者其他處理器不可見的地方,因此讀取volatile變量時,總會返回最新寫入的值。 volatile 只保證了內存的可見性,不保證線程之間的的互斥性
發布一個對象的意思是,是對象能夠在他的作用域之外的代碼中被使用,逸出只的是不該發布的對象被發布出去。
發布一個對象public static Set<Secret> knowSecrets;public void initialize(){ knownSecrets = new HashSet<Secret>();}逸出對象package net.jcip.examples;/** * UnsafeStates * <p/> * Allowing internal mutable state to escape * * @author Brian Goetz and Tim Peierls */class UnsafeStates { private String[] states = new String[]{ "AK", "AL" /*...*/ }; public String[] getStates() { return states; }}在這里大家不要誤解本書作者的意思,在我們的日常開發中pojo類或者我們的domain經常以這種形式出現,但是在這里作者想表達的意思是對于復雜的業務對象來說的,并不是針對pojo對象。業務對象的可變狀態變量的所有操作應該是由這個業務對象自己控制的,而不是把他的私有狀態變量發布出去,這樣會造成很多不確定、不可控的操作,當然在多線程環境中也不是線程安全的。 可控的復雜的業務對象模型(偽代碼)如下。
class ComplexControlStates { private String[] states = new String[]{ "AK", "AL" /*...*/ }; public String[] opration1() { ...一系列復雜的操作 返回 states 的拷貝對象 } public String[] opration2() { ...一系列復雜的操作 返回 states 的拷貝對象 } public String[] opration3() { ...一系列復雜的操作 返回 states 的拷貝對象 }} this的隱式逸出 ThisEscape發布EventListener的時候,ThisEscape實例本身也會溢出,因為這個內部類的實例中包含了對ThisEscape實例的隱含引用。個人理解如下(純屬個人理解):匿名內部類可以訪問外部類的final域,如果final域為一個指向復雜對象的引用,這時候就相當于將這個復雜對象發布了出去,從而造成逸出 package net.jcip.examples;/** * ThisEscape * <p/> * Implicitly allowing the this reference to escape * * @author Brian Goetz and Tim Peierls */public class ThisEscape { public ThisEscape(EventSource source) { source.registerListener(new EventListener() { public void onEvent(Event e) { doSomething(e); } }); } void doSomething(Event e) { } interface EventSource { void registerListener(EventListener e); } interface EventListener { void onEvent(Event e); } interface Event { }} 書中的解釋 在構造過程中使this引用一處的一個常見錯誤是,在構造函數中啟用一個線程,無論是顯示構建(使用構造函數)還是隱式的構建(使用Thread或Runnable),this引用都會被新創建的線程共享,在對象未完全構造完成之前,新的線程可以看見他。當訪問可變的共享變量時,通常需要同步,一種避免使用同步的方法就是不共享,即將變量封閉在單個線程中。這種技術叫做線程封閉。 常見的應用是JDBC的Connection對象。數據庫連接池在將一個connection發布給一個線程之后,在connection還池之前不會再將它分配給其他線程。 詳解 見ThreadLocal對象。
指,維護線程封閉性的職責完全由程序實現來承擔.。 例子: volatile變量上存在一種特殊的線程封閉,如果能保證只有一個線程可以對volatile變量進行寫操作,而其他任意多的變量都只能進行讀操作,這時候就相當于將volatile的寫操作封裝在了線程內,由于volatile變量的內存可見性,可以保證線程安全。
棧封閉是線程封閉的的一種特例。
棧封閉與JVM內存模型相關,當一個線程執行某個方法的時候,JVM會給這個線程單獨開辟一個線程棧空間,里面存儲了這個方法所有局部變量,而且這些變量是被該線程獨享的。
不要濫用ThreadLocal,列如將所有全局變量都作為ThreadLocal對象,或者作為一種隱藏方法參數的手段。ThreadLocal變量類似于全局變量,它能降低代碼的可重用性,并在類之間引入隱含的耦合性,使用時需格外小心。
舉一個在實際開發中遇到的例子。 之前做過一個struts2轉spring mvc的任務,代碼中大量使用了,Struts2的ActionContext對象來獲取Request或session對象,并且耦合在service層甚至是dao層。總之重寫的工作量很大,所以通過一個頂層的Filter來將Request對象保存在ThreadLocal中,并模擬了一個ActionContext對象來代替struts2的ActionContext。不可變對象一定是線程安全的
新聞熱點
疑難解答