Effective Java讀書筆記一:併發(66-73)

衣舞晨風發表於2017-02-02

第66條:同步訪問共享的可變資料

關鍵字synchronized可以保證在同一時刻,只有一個執行緒可以執行某一個方法,或者某一個程式碼塊。

同步不僅可以阻止一個執行緒看到物件處於不一致的狀態中,它還可以保證進入同步方法或者同步程式碼塊的每個執行緒,都看到同一個鎖保護的之前的修改效果。

Volatile 變數可用於提供執行緒安全,但是隻能應用於非常有限的一組用例:多個變數之間或者某個變數的當前值與修改後值之間沒有約束。對變數的寫操作不依賴於當前值,該變數沒有包含在具有其他變數的不變式中。不具有原子性。

多個執行緒共享可變資料的時候,每個讀或寫資料的執行緒都必須執行同步。如果沒有同步,就無法保證一個執行緒所做的修改可以被另一個執行緒獲知。如果需要執行緒之間的互動通訊,而不需要互斥,volatile修飾符就是一種可以接受的形式,但需要正確的使用。

第67條:避免過度同步

依據情況不同,過度同步可能會導致效能降低,死鎖,甚至不確定的行為。

為了避免活性失敗和安全性失敗,在一個被同步的方法或程式碼塊中,永遠不要放棄對客戶端的控制。換句話說,在一個被同步的區域內部,不要呼叫設計成要被覆蓋的方法,或者由客戶端以函式物件的形式提供的方法。

通常,你應該在同步區域內做盡量少的工作。

在這個多核的時代多度同步的實際成本並不是指獲得鎖所花費的CPu時間;而是指失去了並行地機會,以及因為需要確保每個核都有一個一致的記憶體檢視而導致的延遲。過度同步的另一項潛在開銷在於,他會限制Vm優化程式碼的能力。

如果一個可變類要併發使用,應該使這個類程式設計執行緒安全的,通過內部同步,你還可以獲得,明顯比外部鎖定整個物件更高的併發性。否則,就不要在內部同步。讓客戶在必要的時候從外部同步。

反例:
StringBuffer例項幾乎總是被用於單個執行緒中,而它們執行的卻是內部同步。為此,StringBuffer基本都由StringBuilder代替。

第68條:executor和task優先於執行緒

  • 如果編寫的是小程式,或者輕載的伺服器,使用Executor.newCachedThreadPool通常是個不錯的選擇。
  • 在大負載的伺服器中,最好使用Executor.newFixedThreadPool,它為你提供了一個包含固定執行緒數目的執行緒池,或者為了最大限度的控制它,就直接使用ThreadPoolExecutor類。

你不僅應該儘量不要編寫自己的工作佇列,而且還應該儘量不直接使用執行緒。現在的關鍵抽象不再是Thread了,它以前既充當工作單位,又是執行機制。工作單位和工作單位是分開的,現在的關鍵抽象是工作單元,稱作任務(task)。

任務有兩種:Runnable及其近親Callable(它與Runnable類似,但它會返回值)。執行任務的通用機制是executor service。

Executor FrameWork也有一個可以代替java.util.Timer的東西,即ScheduledThreadPoolExecutor。

Timer只有一個執行緒來執行任務,如果timer唯一的執行緒丟擲未被捕捉的異常,timer就會停止工作。而執行緒池executor支援多個執行緒,並且優雅的從丟擲未受檢異常的任務中恢復。

第69條:併發工具優先於wait和notify

java.util.concurrent中更高階的工具分三類:Executor Framework,併發集合(Concurrent Collection)以及同步器(Synchronizer)。

併發集合為標準的集合介面提供了高效能的併發實現,這些實現在內部自己管理同步。因此,併發集合中不可能排除併發活動;將它鎖定沒有什麼作用,只會使程式的速度變慢。

concurrent collections提供了標準容器的高效能併發實現.內部同步和互斥,外部使用,無需加鎖.

優先使用ConcurrentHashMap,而不是Collections.synchronizedMap或者Hashtable,且無需做同步操作.

有的concurrent collections提供了block操作介面,例如BlockingQueue,從中取資料的時候,如果佇列為空,執行緒將等待,新的資料加入後,將自動喚醒等待的執行緒;大部分的ExecutorService都是採用這種方式實現的

簡而言之,我們應該,優先使用java.util.concurrent包中提供的更高階的語言來代替wait,notify.

如果非要用wait和notify,注意以下幾點:

  • wait前的條件檢查,當條件成立時,就跳過等待,可以保證不會死鎖,
  • wait後的檢查,條件不成立繼續等待,可以保證安全
  • 通常情況下都應該使用notifyAll,雖然從優化角度看,這樣不好.

同步器是使一個執行緒能夠等待另一個執行緒的物件。

最常用的同步器是CountDownLatch和Semaphore,不常用的是Barrier 和Exchanger。

倒計數器 鎖存器是一次性障礙,允許一個或者多個執行緒等待一個或者多個其它執行緒來做某些事情。

CountDownLatch的唯一構造器帶一個int型別的引數,這個int引數是指允許所有在等待執行緒被處理之前,必須在鎖存器上呼叫countDown方法的次數。

對於間歇式定時,應該始終使用System.nanoTime而不是System.cucurrentTimeMills。

應該始終使用wait迴圈模式來呼叫wait方法.不要在迴圈外呼叫wait方法.

小結
直接使用 wait和notify,就像 用併發組合語言進行程式設計一樣.而concurrent則提供了更高階的語言。
沒有理由在新程式碼中使用 wait和notify ,即使有,也很少。
如果正在維護使用 wait和notify的程式碼,則儘量在 while迴圈內部呼叫wait。
應該優先使用notifyAll,而不是notify.

第70條:執行緒安全性的文件化

如果你沒有在一個類的文件中描述其行為的併發情況,使用這個類的程式設計師將不得不做出某些假設。如果這些假設是錯誤的,這樣得到的程式就可能缺少足夠的同步,或者過度同步。無論屬於哪種情況,都可能會發生嚴重的錯誤。

一個類為了可被多個執行緒安全使用,必須在文件中清楚地說明它所支援的執行緒安全性級別。

  • 不可變的——這個類的例項是不可變的。這樣的例子包括String,Long,BigInteger。
  • 無條件的執行緒安全——這個類的例項是可變的,但是這個類有足夠的內部同步。例子包括Random,ConconcurrentHashMap。
  • 有條件的執行緒安全——除了有些方法為進行安全的併發使用而需要外部同步之外,這種執行緒安全級別與無條件安全相同。例子包括:Collections.synhronized包裝返回的集合,它們的迭代器要求外部同步。
  • 非執行緒安全——這個類的例項是可變的。為了併發使用它們,客戶必須利用自己選擇的外部同步包圍每個方法呼叫。例子包括ArrayList
  • 執行緒對立的——這個類不能安全地被多個執行緒併發使用,即使所有的方法呼叫都被外圍同步包圍。

類的執行緒安全說明通常放在它的文件中,但帶有特殊執行緒安全屬性的方法則應該在它們自己的文件註釋中說明它們的屬性。

私有鎖只能用在無條件的執行緒安全類上。私有鎖物件模式特別適用於那些專門為繼承而設計的類。如果這種類適用它的例項作為鎖的物件,子類可能很容易在無意中妨礙基類的操作,反之亦然。

第71條:慎用延遲初始化

延遲初始化是延遲到需要域的值時才將它初始化的這種行為。

對於延遲初始化,最好建議“除非絕對必要,否則就不要那麼做”。延遲化降低了初始化類或者建立例項的開銷,卻增加了訪問被延遲初始化的域的開銷。

如果域只是在類的例項部分被訪問,並且初始化這個域的開銷很高,可能就值得進行延遲初始化。

如果出於效能的考慮而需要對靜態域使用延遲初始化,就使用lazy initialization holder class 模式。保證在被用時初始化。

private static class FieldHolder {  
    static final FieldType field = computeFieldValue();  
}  

public static FieldType getField() {  
    return FieldHolder.field;  
}  

如果出於效能的考慮而需要對例項域使用延遲初始化,就使用雙重檢查模式。這種模式避免了在域被初始化之後訪問這個域時的鎖定開銷。

private volatile FieldType field;  

public FieldType getField() {  
    FieldType result = field;  
    if (result == null) {  
        synchronized (this) {  
            result = field;  
            if (result == null) {  
                field = result = computeFieldValue();  
            }  
        }  
    }  
    return result;  
}  

第一次檢查時沒有鎖定,看看這個域是否被初始化;第二次檢查時又鎖定。只有當第二次檢查時標明這個域沒有被初始化,才進行初始化。如果域已經被初始化就不會有鎖定,域被宣告為volatile很重要。

result區域性變數的使用,是為了保證在已經被初始化的情況下,原來的變數只被讀取一次到區域性變數result中,否則在比較的時候需要讀取一次,返回的時候還需要讀取一次。雖然這不是嚴格要求,但是可以提升效能。

簡而言之,大多數的域應該正常的進行初始化,否則,可以參考上面的規則,進行延遲初始化

第72條:不要依賴於執行緒排程器

  • 當有多個執行緒可以執行時,由執行緒排程器決定哪些執行緒將會執行.以及執行多長時間。
  • 任何依賴於執行緒排程器來達到正確性或者效能要求的程式,很有可能都是不可移植的。
  • 要確保可執行執行緒的平均數量不明顯多於處理器的數量。
  • 要編寫健壯,響應良好的,可移植的多執行緒應用程式,最好的辦法是確保可執行執行緒的平均數量不明顯多於處理器的數量。
  • 執行緒優先順序是Java平臺中移植性最差的部分,所以也不要用。

對於大多數程式設計師來說,Thread.yield的唯一用途,就是在測試期間人為的增加程式的併發性。
在Java語言規範中,Thread.yield根本不做實質性工作,只是將控制權返回給它的呼叫者。

小結
不要讓應用程式的併發性依賴於執行緒排程器
不要依賴Thread.yield和執行緒優先順序

第73條:避免使用執行緒組

執行緒組並沒有提供太多有用的功能,而且他們提供的許多功能還都有缺陷的。

如果你正在設計的一個類需要處理執行緒的邏輯組,或許就應該使用執行緒池executor。

《Effective Java中文版 第2版》PDF版下載:
http://download.csdn.net/detail/xunzaosiyecao/9745699

作者:jiankunking 出處:http://blog.csdn.net/jiankunking

相關文章