《Jave併發編程的藝術》學習筆記(1-2章)

Jave併發的藝術


併發編程的挑戰

上下文切換

CPU通過時間片分配算法來循環執行任務,當前時間片執行完之後會切換到下一個任務。但是,切換會保存上一個任務的狀態,一遍下次切換回這個任務時,可以再次加載這個狀態。所以任務從保存到再加載的過程就是一次上下文切換。

如何減少上下文切換

減少上下文切換的方法有無鎖併發編程、CAS算法、使用最少線程和使用協程

  • 無鎖併發編程:多線程競爭鎖時,會引起上下文切換,所以多線程處理數據時,可以用一些辦法避免使用鎖,如將數據ID按照Hash算法取模分段,不同的線程處理不同段的數據
  • CAS算法:Java的Atomic包使用CAS算法來更新數據,而不需要加鎖
  • 使用最少線程:避免創建不需要的線程,比如任務很少,但是創建了很多線程來處理,這樣會造成大量線程都處於等待狀態
  • 協程:在單線程實現多任務的調度,並在單線程裏維持多個任務間的切換

死鎖

死鎖常見的原因是,線程之間相互等待。避免死鎖的幾個常見方法:

  • 避免一個線程獲取多個鎖
  • 避免一個線程在鎖內同時佔用多個資源,儘量保證每個鎖只佔用一個資源
  • 嘗試使用定時鎖,使用lock.tryLock(timeout)來代替使用內部鎖機制
  • 對於數據庫鎖,加鎖和解鎖必須在一個數據庫連接裏,否則會出現解鎖失敗的情況

底層實現原理

volatile

volatile的定義與實現原理

Java編程語言允許線程訪問共享變量,爲了確保共享變量能被準確和一致地更新,線程應該確保通過排他鎖單獨獲取這個變量。所以java提供了volatile,在某些情況下面比鎖更加的方便。如果一個字段被聲明爲volatile,Java線程內存模型確保所有線程看到這個變量的值是一致的。

volatile如何保證可見性:通過Lock前綴指令,對volatile進行寫時,JVM會向處理器發送一條Lock前綴的指令,將這個變量所在緩存行的數據寫回到系統內存

  • Lock前綴指令會引起處理器緩存行寫回到內存
  • 一個處理器的緩存回寫會導致其他處理器的緩存無效

synchronized

synchronized的介紹

併發編程裏面元老級別的存在,重量級的鎖。Java裏面每一個對象都可以作爲鎖

  • 對於普通的方法,鎖是當前實例的對象
  • 對於靜態同步方法,鎖是當前類的Class對象
  • 對於同步方法塊,鎖是Synchronized括號裏配置的對象

Java對象頭

synchronized用的鎖是存在Java對象頭裏面的。對象頭的內容

  • Mark Word:存儲對象的HashCode或鎖信息
  • Class Metadata Address:儲存到對象類型數據的指針
  • Array length:數組的長度

Mark Word裏默認存儲對象的HashCode、分代年齡和鎖標記,Mark Word裏儲存的數據會隨着鎖標誌位的變化而變化。以下是四種狀態的變化

  • 輕量級鎖:指向棧中鎖記錄的指針,鎖標誌位 00
  • 重量級鎖:指向互斥量(重量級鎖)的指針 ,鎖標誌位 10
  • GC標記:空,鎖標誌位 11
  • 偏向鎖:線程ID,Epoch,對象分代年齡,是否是偏向鎖,鎖標誌位 01

鎖的升級與對比

鎖一共有四種狀態,級別從低到高依次是:無鎖狀態、偏向鎖狀態,輕量級鎖狀態和重量級鎖狀態,這幾個狀態會隨着競爭情況逐漸升級。注意:鎖可以升級但是不能夠降級。一些的設計目的只有一個。爲了提高獲得鎖和釋放鎖的效率

  • 偏向鎖:大多數情況下,鎖不僅不存在多線程競爭,而且總是由他同一個線程多次獲得,爲了讓線程獲得鎖的代價更低而引入了偏向鎖。如:當一個線程訪問同步塊並獲取鎖時,會在對象頭和棧幀中的鎖記錄裏存儲鎖偏向的線程ID,以後該線程再次進入和退出同步塊時不需要進行CAS操作來加鎖和解鎖,僅僅判斷一下對象頭裏面的Mark Word裏面是否存儲着指向當前線程的偏向鎖。如果有,表示當前線程已經獲得了鎖;如果沒有,則使用CAS競爭鎖,再嘗試使用CAS將對象頭的偏向鎖指向當前線程
  • 輕量級鎖:線程在執行同步塊之前,JVM會先在當前線程的棧幀中創建用於儲存記錄的空間,並將對象頭中的Mark Word複製到鎖記錄中,官方稱爲Displaced Mark Word。然後線程嘗試使用CAS將對象頭中的Mark Word替換爲指向鎖記錄的指針。如果成功,當前線程就會獲得鎖,如果失敗,表示其他線程競爭鎖,當前線程便可以嘗試通過自旋來獲取鎖,自旋失敗之後,鎖就會膨脹成重量級鎖
  • 重量級鎖:因爲自旋會消耗CPU,爲了避免無用的自旋(如:獲得鎖的線程被阻塞住了),一旦鎖升級爲重量級鎖就不會恢復到輕量級鎖狀態。當鎖出於這個狀態下,其他線程試圖獲取鎖時,都會被阻塞住,當持有鎖的線程釋放鎖之後會喚醒這些線程,被喚醒的線程就會進入新一輪的奪鎖之爭

原子操作的實現原理

原子(atomic),不能被進一步分割的最小粒子,原子操作:不能被中斷的一個或一系列的操作。

處理器如何實現原子操作

  • 第一個機制,通過總線鎖來保證原子性
  • 第二個機制,通過緩存鎖定來保證原子性

Java如何實現原子操作

可以通過鎖,和循環CAS的方式實現原子操作

CAS

CAS(Compare And Swap):比較並且交換。一箇舊值,一個新值,操作之前比較舊值有沒有發生變化,如果沒有發生變化,才換成新值,發生了變化則不交換

CAS存在的三大問題:

  • ABA問題:CAS操作需要在操作的值的時候,檢查值有沒有發生變化。但是如果一個值原來是A,變成了B,又變成了A,使用CAS進行檢查時會發現他的值沒有發生變化,但是實際上他發生了變化。解決方案:給變量加上版本號
  • 循環時間長開銷大:自旋CAS如果不成功,會給CPU帶來非常大的開銷
  • 只能保證一個共享變量的原子操作:JDK提供了AtomicReference類來保證引用對象之間的原子性,就可以把多個變量放在一個對象裏來進行CAS操作
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章