對象的共享
本文介紹如何共享和發布對象,使它們能夠安全地由多個線程同時訪問。
兩篇博文合起來就形成了構建線程安全類以及通過juc類庫構建并發應用程序的重要基礎。
1 可見性
通常,我們無法保證執行讀操作的線程能看到其他線程寫入的值,因為每個線程都由自己的緩存機制。為確保多個線程之間對內存寫入操作的可見性,必須使用同步機制。
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; } }
看起來會輸出42,但事實上很可能根本無法終止,因為讀線程可能永遠看不到ready的值;更奇怪的是可能輸出0,因為讀線程看到了寫入ready的值,卻沒有看到之后寫入number的值,這種現象稱為“重排序”(Reordering)。
在沒有同步的情況下,編譯器、處理器以及運行時等都可能對操作的執行順序進行一些意想不到的調整。
有種簡單方法避免這些復雜的問題:只要有數據在多個線程之間共享,就該使用正確的同步。
1.1 失效數據
除非在每次訪問變量時使用同步,否則很可能獲得變量的一個失效值。失效值可能不會同時出現:一個線程可能獲得一個變量的最新值,而獲得另一個變量的失效值。
失效數據還可能導致一些令人困惑的故障,如:意料之外的異常、被破壞的數據結構、不精確的計算、無限循環等。
//非線程安全的可變整數類 @NotThreadSafe public class MutableInteger { private int value; public int get() { return value; } public void set(int value) { this.value = value; } }
此類非線程安全,因為get和set方法都是在沒有同步的情況下訪問value的失效值很容易出現:若某線程調用set,則另一個正在調用get的線程可能看到更新后的value值,也可能看不到。
//線程安全的可變整數類 @ThreadSafe public class SynchronizedInteger { @GuardedBy("this") private int value; public synchronized int get() { return value; } public synchronized void set(int value) { this.value = value; } }
通過對set,get進行同步,可使此類成為一個線程安全的類。僅對set同步不夠,調用get的線程仍可能看見失效值。
1.2 非原子的64位操作
對于非volatile類型的long和double變量,JVM允許將64位的讀操作或寫操作分解為兩個32位的操作。所以,當讀取該類變量的操作在不同的線程時,很可能會讀取到某個值的高32位和另一個值的低32位,造成讀取到是一個隨機值。除非用關鍵字volatile來聲明它們,或者用鎖保護起來。
1.3 加鎖和可見性
當某線程執行由鎖保護的同步代碼塊時,可以看到其他線程之前在同一同步代碼塊中的所有操作結果。如果沒有同步,將無法實現上述保證。
加鎖的含義不僅僅局限于互斥行為,還包括內存可見性.為了確保所有線程都能看到共享變量的最新值,所有執行讀操作或寫操作的線程都必須在同一個鎖上同步.
1.4 volatile變量
用于確保將變量的更新操作通知到其他線程,訪問volatile變量時不會執行加鎖操作,也就不會使執行線程阻塞,是一種比sychronized更輕量級的同步機制.
編譯器與運行時都會注意到此變量是共享的,因此不會將該變量上的操作與其他內存操作一起重排序.
volatile變量不會被緩存在寄存器或其他處理器不可見的地方,因此在讀取volatile變量時總會返回最新寫入的值.
從內存可見性來看:寫入volatile變量相當于退出同步代碼塊,讀取則相當于進入同步代碼塊(并不建議過度依賴此特性,通常比使用鎖的代碼還復雜)
僅當能簡化代碼的實現及對同步策略的驗證時,才該用.若在驗證正確性時需要復雜判斷可見性,就不要使用!正確使用方式包括:
確保它們自身狀態的可見性
確保它們所引用對象的狀態的可見性
標識一些重要的程序周期事件的發生(如初始化或關閉)
// 數綿羊 volatile boolean asleep; ... while(!asleep){ countSomeSheep(); }
代碼分析
一種典型用法:檢查某個狀態標記判斷是否退出循環.示例中,線程試圖通過數綿羊方法進入休眠狀態.為了使此示例能正確執行,asleep必須為volatile型.否則,當asleep被另一個線程修改時,執行判斷的線程卻發現不了.亦可使用加鎖保證,但代碼會很復雜.
雖然方便,但也存在局限性.常用做某個操作完成,發生中斷或狀態的標志,如上例的asleep標志…但語義不足以確保遞增操作的原子性,除非確保只有一個線程對變量執行寫操作(后文的原子變量常做一種"更好的volatile變量").
加鎖機制既可以確??梢娦杂挚梢源_保原子性,而volatile變量只能確??梢娦?/p>
當且僅當滿足以下所有條件時,才該用volatile變量
對變量的寫入操作不依賴變量的當前值,或能確保只有單個線程更新變量的值
該變量不會與其他狀態變量一起納入不變性條件中
在訪問變量時不需要加鎖
2 發布與逸出
發布:使對象能夠在當前作用域之外的代碼中使用.
發布方式:
將一個指向該對象的引用保存到其他代碼可以訪問的地方(最簡單的就是保存到公有的靜態變量)
非私有方法中返回該引用
將引用傳遞到其他類的方法中
當某個不應該發布的對象被發布時,就被稱為逸出.
//使內部的可變狀態逸出(不要這樣做!!!) class UnsafeStates { private String[] states = new String[]{ "AK", "AL" /*...*/ }; public String[] getStates() { return states; } }
代碼分析
如此發布states有問題,因為任何調用者都能修改這個數組的內容.states已經逸出了它所在的作用域,因為這個本應是private的變量已經被發布了.
//this引用隱式地在構造函數中逸出 public class ThisEscape { public ThisEscape(EventSource source) { source.registerListener(new EventListener() { public void onEvent(Event e) { doSomething(e); } }); } }
代碼分析
當ThisEscape發布EventListener時,也隱含發布了ThisEscape實例本身,因為內部類的實例包含了對外部類實例的隱含引用.
構造過程中,另一個常見錯誤是,在構造器啟動一個線程.此時,無論是顯式創建(傳給構造器)或隱式(內部類),this引用都會被創建的線程共享.在對象尚未完全構造之前,新的線程就可以看見它.在構造器創建線程并無錯誤,但最好不要立即啟動,而是通過start或initialize方法啟動.在構造器調用一個可改寫的實例方法時,也會導致this引用逸出.
想在構造器注冊一個-或啟動線程,可使用一個私有的構造器和一個公共的工廠方法.如下示例:
public class SafeListener { private final EventListener listener; private SafeListener() { listener = e -> doSomething(e); } public static SafeListener newInstance(EventSource source) { SafeListener safe = new SafeListener(); source.registerListener(safe.listener); return safe; } }
3 線程封閉
一種避免使用同步的方式就是不共享數據.
如果僅在單線程內訪問數據,就不需要同步,這就被稱為線程封閉.線程封閉是程序設計中的考慮因素,必須在程序中實現.Java也提供了一些機制幫助維護線程封閉性,比如局部變量和ThreadLocal類.
3.1 Ad-hoc線程封閉
維護線程封閉性的職責完全由程序實現來承擔.
使用volatile變量是實現Ad-hoc線程封閉的一種方式,只要能保證只有單個線程對共享的volatile變量執行寫操作,就可以安全地在這些變量上進行“讀-改-寫”操作,volatile變量的可見性又保證了其他線程能夠看到最新的值。
Ad-hoc線程封閉是非常脆弱的,沒有語言特性可使對象直接封閉到目標線程.因此在程序中盡量少使用.
在可能的情況下,使用其他更強的線程封閉技術.
##3.2 棧封閉
在棧封閉中,只能通過局部變量才能訪問對象.
局部變量的固有屬性之一就是封閉在執行線程中
它們位于執行線程的棧中,其他線程無法訪問此棧.
即使使用了非線程安全的對象,該對象仍然是線程安全的.
3.3 ThreadLocal類
使用ThreadLocal是一種更規范的線程封閉方式,它能使線程中的某個值與保存值的對象關聯起來。提供了get與set等訪問接口方法,這些方法為每個使用該變量的線程都存有一份獨立的副本,因此get總是返回由當前執行線程在調用set時設置的最新值.
常用于防止對可變的單實例變量或全局變量進行共享.
如下示例,通過將JDBC的連接保存到ThreadLocal對象中,每個線程都會擁有屬于自己的連接:
//使用TheadLocal來維持線程封閉性 public class ConnectionDispenser { static String DB_URL = "jdbc:mysql://localhost/mydatabase"; private ThreadLocal
當某個頻繁執行的操作需要一個臨時對象,如一個緩沖區,而同時又希望避免在每次執行時都重新分配該臨時對象,就可以使用該技術.
當某個線程初次調用get方法時,就會調用initialValue來獲取初始值.可將ThreadLocal < T >看作包含了Map< Thread,T>對象,保存了特定于該線程的值,但ThreadLocal的實現并非如此.這些特定于線程的值存在Thread對象中,當線程終止后,這些值會作為垃圾被回收.
ThreadLocal 變量類似于全局變量,它能降低代碼的可重用性,并在類之間引入隱含的耦合性,使用時需要格外小心.
4 不變性
不可變對象:
滿足以下條件:
對象創建以后其狀態就不能修改
對象的所有域都是final類型(final類型域是不能被修改的)
對象是正確創建的(在對象的創建期間,this引用沒有逸出)
在被創建后其狀態就不能被修改,且必線程安全.
在JMM中,final域能確保初始化過程的安全性,從而可以無限制地訪問不可變對象,并在共享這些對象時無須同步.
5 安全發布
任何線程都可在無額外同步情況下安全訪問不可變對象,即使在發布時沒有使用同步.
然而,若final域所指向為可變對象,訪問這些可變對象的狀態時仍需同步.
安全發布常用模式
可變對象必須通過安全方式發布,常意味著發布和使用該對象的線程都需同步.
為安全發布,對象的引用以及對象的狀態必須同時對其他線程可見.
一個正確構造的對象可以通過以下方式來安全發布
在靜態初始化函數里初始化一個對象引用
將對象的引用保存到volatile類型的域或者AtomicReference對象中
將對象的引用保存到某個正確構造對象的final類型域中
將對象的引用保存到一個由鎖保護的域中
線程安全庫中的容器類提供了以下的安全發布保證:
通過將一個鍵或者值放入Hashtable、synchronizedMap或者ConcurrentMap中,可以安全地將它發布給任何從這些容器中訪問它的線程
通過將某個對象放入Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList或者synchronizedSet中,可以將該對象安全地發布到任何從這些容器中訪問該對象的線程
通過將某個對象放入BlockingQueue或者ConcurrentLinkedQueue中,可以將該對象安全地發布到任何從這些隊列中訪問該對象的線程
通常發布一個靜態構造的對象,最簡單安全的方式就是使用靜態的初始化器:
public static Holder holder = new Holder(42);
由JVM在類的初始化階段執行,且由于JVM內部存在著同步機制,因此這樣初始化的任何對象都能被安全發布.
事實不可變對象:對象從技術上來看是可變的,但其狀態在發布后不會再改變.
在沒有額外的同步的情況下,任何線程都可以安全地使用被安全發布的事實不可變對象.
對于可變對象,不僅在發布對象時需要同步,而且在每次對象訪問時同樣需要使用同步來確保后續修改操作的可見性.
對象的發布需求取決于它的可變性:
不可變對象可以通過任意機制來發布。
事實不可變對象必須通過安全方式來發布。
可變對象必須通過安全方式來發布,而且必須是線程安全的或者用某個鎖保護起來。
安全的共享對象
實用策略:
線程封閉 線程封閉的對象只能由一個線程擁有,對象被封閉在該線程中,并且只能由這個線程修改
只讀共享 在沒有額外同步的情況下,共享的只讀對象可以由多個線程并發訪問,但任何線程都不能修改它.共享的只讀對象包括不可變對象和事實不可變對象
線程安全共享 線程安全的對象在其內部實現同步,因此多個線程可以通過對象的公共接口來進行訪問而不需要進一步的同步
保護對象 被保護的對象只能通過持有特定的鎖來訪問.保護對象包括封裝在其他線程安全對象中的對象,以及已發布的并且由某個特定鎖保護的對象
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。