from:http://blog.csdn.net/ilibaba/archive/2009/06/01/4234248.aspx
NO.48 對共用可變資料的同步訪問
同步,不僅可以阻止一個線程看到對象處於不一致的狀態中,它還可以保證通過一系列看似順序執行的狀態轉變序列,對象從一種一致的狀態變遷到另一種一致的狀態。
synchronized關鍵字可以保證在同一時刻,只有一個線程在執行一條語句,或者一段代碼塊。java語言保證讀或寫一個變數是原子的,除非這個變數的類型是long或double.
java的記憶體模型決定,為了線上程之間可靠地通訊,以及為了互斥訪問,對原子資料的讀寫進行同步是需要的。看一個可怕的例子:
- //Broken - require synchronization!
- private static int nextSerialNumber=0;
- public static int generateSerialNumber(){
- return nextSerialNumber++;
- }
對其改進,只需要在generateSerialNumber()的聲明中增加synchronized修飾符即可。
為了終止一個線程,一種推薦的做法是讓線程輪詢某個域,該域的值如果發生變化,就表明此線程就應該終止自己。下面的例子就是這個思路,但在同步出了問題。
- //Broken - requires synchronization
- public class StoppableThread extends Thread{
- private boolean stopRequested=false;
- public void run(){
- boolean done=false;
- while(!stopRequested && !done){
- ...//do what needs to be done in the thread
- }
- }
- public void requestStop(){
- stopRequested=true;
- }
- }
對其改進如下:
- //Properly synchronized cooperative thread temination
- public class StoppableThread extends Thread{
- private boolean stopRequested=false;
- public void run(){
- boolean done=false;
- while(!stopRequested() && !done){
- ...//do what needs to be done in the thread
- }
- }
- public synchronized void requestStop(){
- stopRequested=true;
- }
- private synchronized boolean stopRequested(){
- return stopRequested;
- }
- }
另一種改進是,將stopRequested聲明為volatile,則同步可以省略。
關於Singleton:
再來看延遲初始設定(lazy initialization)問題,雙重訪問模式並不一定都能正常工作,除非被共用的變數包含一個原語值。看例子:
- //The double-check idion fro lazy initialization - broken!
- private static Foo foo=null;
- public static Foo getFoo(){
- if (foo==null){
- synchronized(Foo.class){
- if(foo==null)foo=new Foo();
- }
- }
- return foo;
- }
最容易的修改是省去延遲初始設定:
- //normal static initialization (not lazy)
- private static finall Foo foo=new Foo();
- public static Foo getFoo(){
- return foo;
- }
或者使用正確的同步方法,但可能增加少許的同步開銷:
- //properly synchronized lazy initialization
- private static Foo foo=null;
- public static synchronized Foo getFoo(){
- if(foo==null)foo=new Foo();
- return foo;
- }
按需初始化容器模式也不錯,但是它只能用於靜態域,不能用於執行個體域。
- //The initialize-on-demand holder class idiom
- private static class FooHolder(){
- static final Foo foo=new Foo();
- }
- public static Foo getFoo(){ return FooHolder.foo;}
簡而言之,無論何時當多個線程共用可變資料的時候,每個讀或寫資料的線程必須獲得一把鎖。如果沒有同步,則一個線程所做的修改就無法保證被另一個線程所觀察到。
No.49 避免過多的同步
通常,在同步地區應該做儘可能少的工作。以避免死結。
大量的同步會引起效能損失。
一個類是否應該做成安全執行緒(thread-safe)的還是線程相容(thread-compatible)的,如果類會被用在同步和不同步的環境中,一個合理的方法是同時提供兩個版本。有以下指導原則:
1)一種做法是提供一個封裝類,實現介面,同事將方法調用轉寄給內部對象中對應的方法之前執行適當的同步操作。
2)第二種做法適合於那些不是被設計用來擴充或者重新實現的類。它提供一個未同步的類和一個子類,在子類中只包含一些被同步的方法,它們依次調用到超類中對應的方法上。
如果一個類或者一個靜態方法,依賴於一個可變的靜態域,那麼必須要在內部進行同步,即使它往往只用於單線程。這種情況下,對於客戶要執行外部同步是不可能的,因為不可能保證其他客戶也會執行外部同步。
總之,為了避免死結和資料破壞,千萬不要從同步地區內部調用外來方法。同事,限制同步地區內部的工作量。
No. 50 永遠不要在迴圈的外面調用wait
wait的標準模式:
synchronized(obj){
while(<condition does not hold>)
obj.wait();
…//perform action appropriate to condition
}
總是使用wait迴圈來調用wait方法,永遠不要再迴圈的外面調用wait。在等待之前測試條件,如果條件成立就可以跳過等待。如果條件成立+等待之前notify已經被調用,則無法保證線程總會從等待中醒過來。在等待之後測試,對於確保安全性是必要的。當條件不成立時,有下面一些理由可以使一個線程醒過來:1)另一個線程可能得到了鎖,在調用notify到等待線程醒過來的時刻之間,得到鎖的線程已經改變了被保護的狀態; 2)條件沒有成立,有線程意外調用了notify;3)通知線程在喚醒的時候,使用了notifyAll;4)偽喚醒,儘管可能很少。
NO.51 不要依賴於線程調度器
不能讓應用程式的正確性依賴於線程調度器。否則,結果得到的應用程式既不健壯也不具有可移植性。作為一個推論,不要依賴Thread.yield或者線程優先順序。這些設施都只是影響到調度器,它們可以被用來提高一個已經能夠正常工作的系統的服務品質,但永遠不應用來“修正”一個原本並不能工作的程式。
編寫健壯的、響應良好的、可移植的多線程應用程式的最好辦法是,儘可能確保在任何給定時刻只有少量的可運行線程。這種辦法採用的主要技術是,讓每個線程做少量的工作,然後使用Object.Wait等待某個條件發生,或者使用Thread.sleep睡眠一段時間。
NO.52 執行緒安全性的文檔化
每個類都應該清楚地在文檔中說明它的安全執行緒屬性。在一個方法的聲明中出現synchronized修飾符,這是一個實現細節,並不是匯出的API文檔的一部分。
一個類為了可被多個安全執行緒地使用,必須在文檔中清楚地說明它所支援的執行緒安全性層級。
1) 非可變性(immutable)-這個類的執行個體對於其它客戶而言是不變的,不需要外部的同步。參見13條。
2)線程程安全的(thread-safe)-這個類的執行個體是可變的,但是所有的地方都包含足夠的同步手段,這些執行個體可以被並發使用無需外部同步。
3) 有條件的安全執行緒(conditionally thread-safe)-這個類(或關聯的類)包含有某些方法,它們必須被順序調用,而不能受到其它線程的幹擾,除此之外,這種安全執行緒層級與上一種情形相同。為了消除被其他線程幹擾的可能性,客戶在執行此方法序列期間,必須獲得一把適當的鎖。如HashTable或Vector,它們的迭代器要求外部同步。如:
- Hashtable h=...;
- synchronized(h){
- for(Enumeration e=h.keys();e.hasMoreElements();)
- f(e.nextElement());
- }
4)線程相容的(thread-compatible)-在每個方法調用的外圍使用外部同步,此時這個類的執行個體可以被安全的並發使用。如ArrayList或HashMap
5)線程對立的(thread-hostile)這個類不能安全地被多個線程並發使用,即使所有的方法調用都被外部同步包圍。通常情況下,線程對立的根源在於,這個類的方法要修改待用資料,而這些待用資料可能會影響到其它的線程。
對於有條件的安全執行緒類,在文檔中指明“為了允許方法調用序列以原子方式執行,哪一個對象應被鎖住”。
NO.53 避免使用線程組
除了線程、鎖和監視器之外,線程系統還提供了一個基本的抽象,即線程組(thread-group)。然而線程組並沒有提供太多有用的功能。
一個例外是,當線程組中的一個線程拋出一個未被捕獲的異常時,ThreadGroup.uncaughtException方法會被自動調用。“執行環境”使用這個方法,以便用適當的方式來響應未被捕獲的異常。
NO.54 保護性地編寫readObject方法
編寫一個類的readObject方法,相當於編寫一個公有的建構函式,無論給它傳遞一個什麼樣的位元組流,它都必須產生一個有效執行個體。下面是縮寫健壯的readObject方法的指導原則:
①對於對象參考網域必須保持為私人的類,對“將被儲存到這些域中的對象”進行保護性拷貝。非可變類的可變組件就屬於這一類別。
②對於具有約束條件的類,一定要檢查約束條件是否滿足,如果不滿足的話,則拋出一個InvalidObjectException異常。這些檢查應跟在所有的保護性拷貝之後。
③如果在對象圖被還原序列化之後,整個對象圖必須都是有效,則應該使用ObjectInputValidation介面。
④無論是直接方式還是間接方式,都不要調用類中可被改寫的方法。
⑤readResolve方法有可能被用來替代保護性的readObject方法。
不嚴格地說,readObject是一個“用位元組流作為唯一參數”的建構函式。當面對一個人工偽造的位元組流的時候,readObject產生的對象會違反它所屬的類的約束條件。初步的方法,是在readObject方法進行約束性檢查,如下例:
- private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException{
- s.defaultReadObject();
- //Check that our invariants are satisfied
- if(start.compareTo(end)>0) throw new InvalidObjectException(start+" after "+ end);
- }
對上述的防範仍可進行攻擊:偽造一個位元組流,這個位元組流以一個有效Period執行個體所產生的位元組流作為開始,然後附加上兩個額外的引用,指向 Period執行個體中的兩個內部私人Date域,攻擊者通過引用攻擊內部域。所以,當一個對象被還原序列化的時候,對於客戶不應該擁有的對象引用,如果哪個域包含了這樣的對象引用,則必須要做保護性拷貝,這是非常重要的。如下例:
- private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException{
- s.defaultReadObject();
- start=new Date(start.getTime());
- end=new Date(end.getTime());
- if(start.compareTo(end)>0) throw new InvlaidObjectException(start+" after "+end);
- }
NO.57 必要時提供一個readResolve方法
無論是 singleton,或是其他執行個體受控(instance-controlled)的類,必須使用readResolve方法來保護“執行個體-控制的約束 ”。從本質上來講,readResovle方法把一個readObject方法從一個事實上的公有建構函式變成一個事實上的公有靜態工廠。對於那些禁止包外繼承的類而言,readResolve方法作為保護性的readObject方法的一種替代,也是非常有用的。
如下sigleton類:
- public class Elvis{
- public static final Elvis INSTANCE = new Elvis();
- private Elvis(){
- ...
- }
- ...//remainder omitted
- }
如果Elvis執行個體序列化介面,則下面的readResolve方法足以保證它的singleton屬性。
- private Object readResolve() throws ObjectStreamException{
- //return the one true elvis and let the GC take care of the Elvis impersonator
- return INSTANCE;
- }
不僅僅對於singleton對象是必要的,readResolve方法對於所有其它的執行個體受控類(如型別安全枚舉類型)也是必需的。
readResolve方法的第二個用法是,就像在第56條建議的那樣,作為保護性的readObject方法的一種保守的替代選擇。此時,第56條中的readObject方法可以下例的例子替代:
- //the defensive readResolve idiom
- private Object readResolve() throws ObjectStreamException(){
- return new Period(start,end);
- }
對於那些允許繼承的類,readResolve方法可能無法替代保護性的readObject方法。如果超類的readResolve方法是final 的,則使得子類執行個體無法被正常地還原序列化。如果超類的readResolve方法是可改寫的,則惡意的子類可能會用一個方法改寫它,該方法返回一個受損的執行個體。
結束語:花了好幾個月斷斷續續地把這本書看完了,期間做了好幾個項目,忙了一段時間,看書的進度有些拖延,現在看之前整理的筆記都有點遺忘了,不過沒關係,以後可以返回去看看,溫故而知新嘛。。。
——————
改動了錯別字,添加了我偶爾看到的漏了的幾個rules。
感想,effective XXX系列給我的感覺總是囉嗦,看完之後覺得裨益不大,可能和實際問題結合得不是很好,一大堆的文字總是讓我覺得眼皮沉重。很感謝有熱心的讀者寫了筆記,雖然看筆記都讓我覺得難以消化。可能在等待一個契機才能讓我明白書中的真諦,可惜在這麼一個速食而且殘酷的社會,沒有人會允許你花時間prepare自己。
花了2,3天看完筆記+一部分書之後,感覺是浪費了現在的寶貴複習時間。不過複習,也不知道是否徒勞。那麼多公司的鄙視,周圍人的compare,真的讓我陷入depression。