要想理解 synchronized 原理,必須要了解 cas 和 使用者態、核心態的理論
synchronized 是關鍵字,具體怎麼實現要翻 cpp、彙編程式碼,記住理論就行了
CAS
全稱叫 Compare And Swap 或者 Compare And Set,比較並交換、比較並設定。具體是:在執行操作之前,先比較當前記憶體中的值是否等於期望值,如果相等,則執行修改操作;如果不相等,則不執行修改操作,繼續進行比較,直到記憶體中的值與期望值相等為止
舉個例子,比如變數 a = 1,要做 a++ 操作
- 執行緒讀取 a 的值,儲存在自己的棧內(每個執行緒有自己的棧空間,變數的值儲存在 cpu 暫存器)
- 現在已經拿到 a 的值了,做 a++,結果是 2
- 把 2 回寫到 變數 a 中,不是直接回寫,先再次讀取a的值,判斷是否符合條件才決定是否回寫
- 再次讀取 a 的值還是1(說明沒有別的執行緒修改),就把2回寫
- 如果再次讀取的值不是1了,說明別的a已經被別的執行緒修改了,2就不會回寫
- 如果重新讀取的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 一啟動就開啟偏向鎖,鎖升級過程如下
- 建立物件 obj,這時是無鎖,物件頭是 obj 的資訊,沒有和鎖相關的資訊
- 遇到 synchronized 時,加鎖,這時加的是偏向鎖
- 具體是物件頭裡 markword 的執行緒id改為當前持有鎖的執行緒 id
- 物件頭有哪個執行緒id,就表示這個物件被哪個執行緒持有鎖
- 後續如果有重入或競爭先對比執行緒 id,如果 id 一致,沒有競爭,一直是偏向鎖
- 如果有執行緒競爭,升級為自旋鎖
- 也叫輕量級鎖、無鎖(這個無鎖指的是沒加重量級鎖),CAS 實現,別的執行緒一直自旋等待
- 具體是別的執行緒一直CAS自旋嘗試將物件頭的 markword 的執行緒 id 設定成自己的,如果設定成功就表示持有鎖了
- 如果競爭加劇,升級為重量級鎖
- 哪種條件視為競爭加劇?
- jdk1.6及以前:自旋次數超過10次,或自選執行緒數量超過cpu核數一半,自選次數可配置,--XX:PreBlockSpin
- jdk1.6 之後:由 JVM 自己判斷自旋次數、自旋執行緒數是多少時視為競爭加劇
- 和核心態通訊,向作業系統申請資源
- 哪種條件視為競爭加劇?
為什麼有輕量級鎖還要重量級鎖?執行緒自旋是要消耗 CPU 的,當執行緒數量過多顯然不適合繼續自旋。重量級鎖會把這些等待的執行緒放進一個佇列,這個佇列裡面的執行緒不會消耗 CPU