synchronized

CyrusHuang發表於2024-08-30

要想理解 synchronized 原理,必須要了解 cas 和 使用者態、核心態的理論

synchronized 是關鍵字,具體怎麼實現要翻 cpp、彙編程式碼,記住理論就行了

CAS

全稱叫 Compare And Swap 或者 Compare And Set,比較並交換、比較並設定。具體是:在執行操作之前,先比較當前記憶體中的值是否等於期望值,如果相等,則執行修改操作;如果不相等,則不執行修改操作,繼續進行比較,直到記憶體中的值與期望值相等為止

舉個例子,比如變數 a = 1,要做 a++ 操作

  1. 執行緒讀取 a 的值,儲存在自己的棧內(每個執行緒有自己的棧空間,變數的值儲存在 cpu 暫存器)
  2. 現在已經拿到 a 的值了,做 a++,結果是 2
  3. 把 2 回寫到 變數 a 中,不是直接回寫,先再次讀取a的值,判斷是否符合條件才決定是否回寫
    1. 再次讀取 a 的值還是1(說明沒有別的執行緒修改),就把2回寫
    2. 如果再次讀取的值不是1了,說明別的a已經被別的執行緒修改了,2就不會回寫
  4. 如果重新讀取的a不是1,重新做a++,重做的順序和上面一樣,直到成功+1

這個過程是有問題的,比較經典的是 ABA 問題,這個可以透過版本號解決

除了 ABA 還有個更深入的問題,a 一開始是1,a++ 做完,對比 a,然後把新的值寫入 a 的過程中,對比和寫入是必須保證原子性的,這條命令不能被打斷,這是怎麼保證的?java 呼叫 c++,c++ 呼叫匯編,給這條指令上了個鎖(要證明就要去翻 jvm 的 cpp 程式碼,還要能看懂彙編程式碼)

使用者態、核心態

這是作業系統的兩種執行模式,不同模式有不同命令,核心態的命令一般程式不能直接呼叫執行,需要經過作業系統,作業系統執行這些高階別的命令。JVM 是已使用者態的模式在執行,想要加一把重量級鎖,這個只能使用者態跟作業系統發起申請,作業系統接收到指令然後呼叫核心態的指令,這個開銷是比較大的

比如 java 不能直接操作記憶體,只能間接透過 UnSafe 類訪問,比如前端 js 不能作業系統檔案,再比如普通程式不能直接修改作業系統核心結構等等

synchronized

加鎖的方式就不說了,同步方法,同步程式碼塊等

synchronized 可以給類加鎖,可以給物件加鎖,究竟是怎麼加的?修改物件頭資訊,修改成功就表示加鎖成功,可以使用 JOL 分析物件記憶體分佈

jvm 啟動 4 秒後開啟偏向鎖,可以在同步程式碼塊中對一個物件加鎖(比如 obj),程式啟動在主方法中使用 JOL 分析物件記憶體分佈,休眠5秒,然後呼叫同步程式碼塊的方法,再使用 JOL 列印記憶體分佈,會發現兩次答應的物件頭不一樣

也可以配置延遲時間為0,jvm 一啟動就開啟偏向鎖,鎖升級過程如下

  1. 建立物件 obj,這時是無鎖,物件頭是 obj 的資訊,沒有和鎖相關的資訊
  2. 遇到 synchronized 時,加鎖,這時加的是偏向鎖
    1. 具體是物件頭裡 markword 的執行緒id改為當前持有鎖的執行緒 id
    2. 物件頭有哪個執行緒id,就表示這個物件被哪個執行緒持有鎖
    3. 後續如果有重入或競爭先對比執行緒 id,如果 id 一致,沒有競爭,一直是偏向鎖
  3. 如果有執行緒競爭,升級為自旋鎖
    1. 也叫輕量級鎖、無鎖(這個無鎖指的是沒加重量級鎖),CAS 實現,別的執行緒一直自旋等待
    2. 具體是別的執行緒一直CAS自旋嘗試將物件頭的 markword 的執行緒 id 設定成自己的,如果設定成功就表示持有鎖了
  4. 如果競爭加劇,升級為重量級鎖
    1. 哪種條件視為競爭加劇?
      1. jdk1.6及以前:自旋次數超過10次,或自選執行緒數量超過cpu核數一半,自選次數可配置,--XX:PreBlockSpin
      2. jdk1.6 之後:由 JVM 自己判斷自旋次數、自旋執行緒數是多少時視為競爭加劇
    2. 和核心態通訊,向作業系統申請資源

為什麼有輕量級鎖還要重量級鎖?執行緒自旋是要消耗 CPU 的,當執行緒數量過多顯然不適合繼續自旋。重量級鎖會把這些等待的執行緒放進一個佇列,這個佇列裡面的執行緒不會消耗 CPU

相關文章