1. 程式人生 > >併發程式設計的藝術-筆記

併發程式設計的藝術-筆記

併發程式設計的藝術

  • 多執行緒一定快麼
    • 什麼是上下文切換
    • 如何減少上下文切換
      • 無鎖併發程式設計(如將資料的ID按照Hash演算法取模分段,不同的執行緒處理不同段的資料)
      • CAS演算法(使用Atomic包)
      • 使用最少執行緒(避免建立不需要的執行緒)
      • 協程(在單執行緒裡實現多工的排程,並在單執行緒裡維持多個任務間的切換)
    • 減少上下文切換實戰
  • 如何避免死鎖
    • 避免一個執行緒同時獲取多個鎖
    • 避免一個執行緒在鎖內同時佔用多個資源,儘量保證每個鎖佔用一個資源
    • 嘗試使用定時鎖,使用lock.tryLock(timeout)來替代使用內部鎖機制
    • 對於資料庫鎖,加鎖和解鎖必須在一個數據庫連線裡,否則會出現解鎖失效的情況
  • 資源限制
    • 什麼是資源限制
      • 在併發程式設計中,將程式碼執行速度加快的原則是將程式碼中序列執行的部分變成併發執行,但是如果將某段序列的程式碼併發執行,因為受限於資源,仍然在序列執行,這時候程式不僅不會加快執行,反而會更慢,應為增加了上線文切換和資源排程的時間。
    • 如何解決資源限制問題
      • 對於硬體資源限制,可以考慮使用叢集並行執行程式。既然單機的資源有限制,那麼就讓程式在多機上執行。比如使用ODPS、Hadoop或者自己搭建伺服器叢集,不同的機器處理不同的資料。可以通過“資料ID%機器數”,計算得到一個機器編號,然後由對應編號的機器處理這筆資料。
      • 對於軟體資源限制,可以考慮使用資源池將資源複用。比如使用連線池將資料庫和Socket連線複用,或者在呼叫對方webservice介面獲取資料時,只建立一個連線。
    • 在資源限制的情況下進行併發程式設計
      • 根據不同的資源限制調整程式的併發度。
  • synchronized
    • Java中的每一個物件都可以作為鎖
      • 普通同步方法-鎖是當前例項物件
      • 靜態同步方法-鎖是當前類的Class物件
      • 同步方法塊-鎖是Synchronized括號裡配置的物件
    • JVM基於進入和退出Monitor物件來實現方法同步和程式碼塊同步
      • 程式碼塊同步通過monitorenter和monitorexit指令實現
      • monitorenter指令是在編譯後插入到同步程式碼塊的開始位置,而monitorexit是插入到方法結束處和異常處,JVM要保證每個moniterenter必須有monitorexit與之配對。任何物件都有一個monitor與之關聯,當且一個monitor被持有後,它將處於鎖定狀態。執行緒執行到monitorenter命令時,將會嘗試獲取物件所對應的monitor的所有權,即嘗試獲得物件的鎖。
    • synchronized用的鎖是存在物件頭裡的

https://img.mubu.com/document_image/3c60e806-5df6-4c45-ab17-79cb6778a6e5-67246.jpg

  • 鎖的升級
    • Java SE1.6中,鎖一共有4中狀態:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態。
    • 這幾個狀態會隨著競爭情況逐漸升級,鎖可以升級但不能降級。這種策略是為了提高獲得鎖和釋放鎖的效率。
    • 偏向鎖
      • 大多數情況下,鎖不僅不存在多執行緒競爭,而且總是由同一執行緒多次獲得,為了讓執行緒獲得鎖的代價更低而引入了偏向鎖。
      • 偏向鎖使用了一種等到競爭出現才釋放鎖的機制,所以當其他執行緒嘗試競爭偏向鎖時,持有偏向鎖的執行緒才會釋放鎖。
    • 小總結
      • 在所有的鎖都啟用的情況下執行緒進入臨界區時會先去獲取偏向鎖,如果已經存在偏向鎖了,則會嘗試獲取輕量級鎖,啟用自旋鎖,如果自旋也沒有獲取到鎖,則使用重量級鎖,沒有獲取到鎖的執行緒阻塞掛起,直到持有鎖的執行緒執行完同步塊喚醒他們;偏向鎖是在無鎖爭用的情況下使用的,也就是同步開在當前執行緒沒有執行完之前,沒有其它執行緒會執行該同步塊,一旦有了第二個執行緒的爭用,偏向鎖就會升級為輕量級鎖,如果輕量級鎖自旋到達閾值後,沒有獲取到鎖,就會升級為重量級鎖;如果執行緒爭用激烈,那麼應該禁用偏向鎖。
    • 鎖的優缺點對比

https://img.mubu.com/document_image/b3b60cef-5518-4411-9ee5-7ff3b725ac8e-67246.jpg

  • Java如何實現原子操作
    • 使用迴圈CAS實現原子操作
    • 使用鎖機制實現原子操作
  • 併發程式設計模型的兩個關鍵問題
    • 執行緒之間如何通訊
      • 通訊是指執行緒之間以何種機制來交換資訊。
        • 在指令式程式設計中,執行緒之間的通訊機制有兩種:共享記憶體和訊息傳遞。
        • 在共享記憶體的併發程式設計模型裡,執行緒之間共享程式的公共狀態,通過寫-讀記憶體中的公共狀態進行隱式通訊。
        • 在訊息傳遞的併發程式設計模型裡,執行緒之間必須通過傳送訊息來顯式進行通訊。
    • 執行緒之間如何同步
      • 同步是指程式中用於控制不同執行緒間操作發生相對順序的機制。
      • 在共享記憶體併發程式設計模型裡,同步是顯示進行的。程式設計師必須顯示指定某個方法或程式碼片段需要線上程之間互斥執行。
      • 在訊息傳遞的併發程式設計模型裡,由於訊息的傳送必須在訊息的接收之前,因此同步是隱式進行的。
    • Java的併發採用的是共享記憶體模型,Java執行緒之間的通訊總是隱式進行。
  • Java記憶體模型(JMM

https://img.mubu.com/document_image/42c3cde0-7ffb-45b1-9f52-4170ef46121b-67246.jpg

    • 如果執行緒A與執行緒B之間要通訊的話,必須要經歷下面兩個步驟:
      • 執行緒A把本地記憶體中A種更新過的共享變數重新整理到主記憶體中去。
      • 執行緒B到主記憶體中去讀取執行緒A之前已經更新過的共享變數。
    • JMM通過控制主記憶體與每個執行緒的本地記憶體之間的互動來提供記憶體可見性保證。
  • volatile
    • voltaile的記憶體語義
      • 讀:當讀一個volatile變數時,JMM會把該執行緒對應的本地記憶體置為無效。執行緒接下來將從主記憶體中讀取共享變數。
      • 寫:當寫一個volatile變數時,JMM會把該執行緒對應的本地記憶體中的共享變數值重新整理到主記憶體。
    • voltaile和指令重排序
      • 編譯器不會對volatile讀與讀後邊的任意記憶體操作重排序。
      • 編譯器不會對volatile寫與寫前面的任意記憶體操作重排序。
  • 鎖的釋放和獲取的記憶體語義
    • 鎖釋放與volatile寫有相同的記憶體語義,鎖獲取與volatile讀有相同的記憶體語義。
    • 執行緒A釋放一個鎖,實質上是執行緒A向接下來將要獲取這個鎖的某個執行緒發出了(執行緒A對共享變數所做修改的)訊息。
    • 執行緒B獲取一個鎖,實質上是執行緒B接收了之前某個執行緒發出的(在釋放這個鎖之前對共享變數所做修改的)訊息。
    • 執行緒A釋放鎖,隨後執行緒B獲得這個鎖,這個過程實質上是執行緒A通過主記憶體向執行緒B傳送訊息。
  • concurrent包的實現

https://img.mubu.com/document_image/f7efd1fa-eab6-4ee4-ab14-c8944896f52e-67246.jpg

    • compareAndSet()方法呼叫同時具有volatile讀和volatile寫的記憶體語義
    • 因此Java執行緒之間的通訊現在有了下面4種方式:
      • ①A執行緒寫volatile變數,隨後B執行緒讀這個volatile變數。
      • ②A執行緒寫volatile變數,隨後B執行緒用CAS更新這個volatile變數。
      • ③A執行緒用CAS更新一個volatile變數,隨後B執行緒用CAS更新這個volatile變數。
      • ④A執行緒用CAS更新一個volatile變數,隨後B執行緒讀取這個volatile變數。
    • concurrent包通用化的實現模式:
      • 首先,宣告共享變數為volatile。
      • 然後,使用CAS的原子條件更新來實現執行緒之間的同步。
      • 同時,配合以volatile的讀/寫和CAS所具有的volatile讀和寫的記憶體語義來實現執行緒之間的通訊。
  • final的記憶體語義
    • 通過為final域增加寫和讀重排序規則,可以為Java程式設計師提供初始化安全保證:只要物件是正確構造的(被構造物件的引用在建構函式中沒有“逸出”),那麼不需要使用同步(指lock和volatile的使用)就可以保證任意執行緒都能看到這個final域在建構函式中被初始化之後的值。
  • happens-before

https://img.mubu.com/document_image/04bc25c6-36d0-4c72-b5d8-10cffebce757-67246.jpg

  • Java中的鎖
    • 重入鎖
      • synchronized
      • ReentrantLock
    • 公平鎖
      • 公平鎖保證了鎖的獲取按照FIFO原則,而代價是進行大量的執行緒切換。非公平鎖雖然可能造成執行緒“飢餓”,但極少的執行緒切換,保證了其更大的吞吐量。
    • 讀寫鎖
      • Java併發包提供讀寫鎖的實現是ReentrantReadWriteLock。ReentrantReadWriteLock不支援鎖升級(持有讀鎖、獲取寫鎖,最後釋放讀鎖的過程)。目的是保證資料可見性,如果讀鎖已被多個執行緒獲取,其中任意執行緒成功獲取了寫鎖並更新了資料,則其更新對其他獲取到讀鎖的執行緒是不可見的。
      • 大多數場景讀是多於寫的。在讀多於寫的情況下,讀寫鎖能夠提供比排它鎖更好的併發效能和吞吐量。
  • 併發容器
    • ConcurrentHashMap
    • ConturrentLinkedQueue非阻塞的方式實現執行緒安全佇列
  • Fork/Join框架
  • Java中的13個原子操作類
    • 原子更新基本型別類
      • AtomicBoolean
      • AtomicInteger
      • AtomicLong
    • 原子更新陣列
      • AtomicIntegerArray
      • AtomicLongArray
      • AtomicReferenceArray
    • 原子更新引用型別
      • AtomicReference
      • AtomicReferencFieldUpdater
      • AtomicMarkableReference
    • 原子更新欄位類
      • AtomicIntegerFieldUpdater
      • AtomicLongFieldUpdater
      • AtomicStampedReference
  • Java中的併發工具類
    • CountDownLatch
    • CyclicBarrier
    • semaphore
    • Exchanger
  • Java中的執行緒池
  • Executor框架
  •  
  • 總結:
    • 使用JDK併發工具包