天天看點

EffectiveJava(筆記九) 并發66. 同步通路共享的可變資料67. 避免過渡同步68. executor和task優先于線程69. 并發工具優先于wait和notify70. 線程安全性的文檔化71. 慎用延遲初始化

66. 同步通路共享的可變資料

關鍵字synchronized可以保證在同一時刻, 隻有一個線程可以執行某一個方法, 或者某一個代碼塊, 為了線上程之間進行可靠的通信, 也為了互斥通路, 同步是必要的, 多個線程共享可變資料的時候, 每個讀或者寫資料的線程都必須執行同步, 未能同步共享可變資料會造成程式的活性失敗和安全性失敗

67. 避免過渡同步

第66條告誡我們缺少同步的危險性, 本條目則關注相反的問題, 過渡同步可能會導緻性能降低, 死鎖甚至不确定的行為

雖然自從Java平台早期以來, 同步的成本已經下降了, 但更重要的是, 永遠不要過度同步, 在這個多核時代, 過度同步的實際成本并不是指擷取鎖所花費的CPU時間, 而是指失去了并行的機會, 以及因為需要確定每個核都有一個一緻地記憶體視圖而導緻的延遲, 過度同步的另一項潛在開銷在于, 它會限制VM優化代碼執行的能力

總之, 為了避免死鎖和資料破壞, 千萬不要從同步區域内部調用外來方法, 簡而言之, 就是要盡量限制同步區域内部的工作量, 當在設計一個可變類的時候, 要考慮一下它們是否應該自己完成同步操作, 在這個多核的時代, 這比永遠不要過度同步來得更重要

68. executor和task優先于線程

executor提供了豐富的線程池靜态工廠, 以滿足各種任務需求

ExecutorFramework也有一個可以代替java.util.Timer的東西, 即ScheduledThreadPoolExecutro, 雖然timer使用起來更加容易, 但是被排程的線程池executro更加靈活, timer隻用一個線程來執行任務, 這在面對長期運作的任務時, 會影響到定時的準确性, 如果timer唯一的線程抛出未被捕獲的異常, timer就會停止執行, 被排程的線程池executor支援多個線程, 并且優雅地從抛出未受檢異常的任務中恢複

69. 并發工具優先于wait和notify

既然正确地使用wait和notify比較困難, 就應該用更進階的并發工具來代替, java.util.concurrent中更進階的工具分成三類: Executor Framework, 并發集合以及同步器

并發集合為标準的集合接口(List Queue Map)提供了高性能的并發實作, 為了提供高并發性, 這些實作在内部自己管理同步, 是以, 并發集合中不可能排除并發活動, 将它同步鎖定沒有什麼作用, 隻會使程式的速度更慢, 并發集合有ConcurrentMap

70. 線程安全性的文檔化

線程安全性級别:

* 不可變的: 這個類的執行個體是不變得, 是以, 不需要外部的同步, 如: String Long BigInteger

* 無條件的線程安全: 這個類的執行個體是可變的, 但是這個類有着足夠的内部同步, 是以, 它的執行個體可以被并發使用, 無需任何外部同步, 如果: Random和ConcurrentHashMap

* 有條件的線程安全: 除了有些方法為進行安全的并發使用而需要外部同步之外, 這種線程安全級别與無條件的線程安全相同, 如: Collections.synchronized包裝傳回的集合, 它們的疊代器要求外部同步

* 非線程安全: 這個類的執行個體是可變, 為了并發地使用它們, 客戶必須利用自己選擇的外部同步包圍每個方法調用, 如: ArrayList和HashMap

* 線程對立的: 這個類不能安全地被多個線程病房使用, 即使所有的方法調用都被外部同步包圍, 線程對立的根源通常在于, 沒有同步地修改靜态資料, 在Java平台類庫中, 線程對立的類或者方法非常少, 有也被廢除了

71. 慎用延遲初始化

延遲初始化是延遲到需要域的值時才将它初始化的這種行為, 如果永遠不需要這個值, 這個域就永遠不會被初始化, 這種方法既适用于靜态域, 也适用于執行個體域, 延遲初始化就像一把雙刃劍, 它降低了初始化類或者建立執行個體的開銷, 卻增加了方法被延遲初始化的域的開銷, 根據延遲初始化的域最終需要初始化的比例以及初始化這些域要多少開銷, 以及每個域多久會被通路一次, 延遲初始化實際降低了性能

延遲初始化有它的好處, 如果域隻在類的執行個體部分被通路, 并且初始化這個域的開銷很高, 可能就值得進行延遲初始化

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

這段代碼可能看起來似乎有些費解, 尤其對于需要用到局部變量result可能有點不解, 這個變量的作用是確定field隻在已經被初始化的情況下讀取一次, 雖然這不是嚴格需要, 但是可以提升性能, 這個方法要比沒有局部變量的方法快25%