面試關於鎖相關回答點

1、爲什麼需要加鎖
爲了提交CPU的使用效率,會在CPU裏面開闢一個高速緩存區或者是寄存器區,在程序運行的時候提前將主存的數據讀入到緩存區中。對於同一個可變的共享變量,每一個線程都會拷貝一個到自己的高速緩存區內,如果一個線程改變了這個變量並不會馬上將數據刷到主存,就這會造成數據修改不一致性。
2、常見的鎖
爲了保證程序的數據的可見性,原子性和禁止從排序就有了volatile和鎖的概念。

先說一下volatile 相比鎖更輕量級一些,它能保證數據的可見性和禁止CPU的指令重排。每一次修改數據時候就會將數據刷到主存,同時每個線程拷貝的變量也會失效。volatile修飾的變量的前面加了一個LOCK前綴指令,就相當於一個內存屏障。

synchronized關鍵字,可以修飾靜態方法,修飾實例方法和代碼塊。當修飾一個靜態方法的時候則鎖的是當前類的Class對象。如果修飾的是實例方法那鎖的就是當前對象(就是同一個對象調用這個方法纔會出現互斥性)。
修飾代碼塊和修飾方法還是有區別的。
修飾方法在字節碼中會生成一個ACC_SYNCHRONIZED標誌,會在方法執行的時候自動生成monitorenter指令,在程序運行結束或者異常的時候會生成一個mointorexit指令。
而修飾代碼塊則會直接生成一個mointerenter指令和兩個mointorexit指令,但是兩個指令只會走一個,分別對應正常結束和異常結束釋放鎖。
還有一個常用的鎖就是Reentrantlocak,內部是是通過AQS來實現的。

3、鎖的底層實現
創建一個對象的時候,對象在堆內存中主要分爲三個部分,對象頭,實例數據和對齊填充。對象頭裏面有一個標記字段,存了一個monitor(相當於一個同步工具)對象,通過monitor對象調用monitor方法返回一個ObjectMonitor對象,因此可以說每一個對象都可以作爲鎖。
ObjectMointer對象裏面主要包括四個參數owner(當前那個線程獲取到鎖)、waitSet(等待線程的隊列)、entryList(處於block狀態下的線程隊列)、count(當前線程獲得到鎖的次數)
當對個線程競爭鎖的時候就會進入entryList隊列當中進行阻塞,當有一個線程獲取到了鎖,則將owner指向它,並且count加一,當這個線程執行了wait方法則會進入waitSet隊列中,count減一,entryList裏面的線程爭奪線程鎖。

4、jdk6之後鎖的優化
鎖的阻塞和喚醒需要從用戶態轉換到內核態,是比較消耗系統資源的,爲了減少這種消耗在jdk6對鎖做了優化。
1、鎖的自旋。
當有已有對象獲取到鎖了,另外一個線程也來獲取鎖,此時不會立馬進入阻塞,而是等待一段時間的自旋等待上一個線程釋放鎖。

2、偏向鎖(在多數情況下,不僅只有沒有多線程競爭,而且總是一個線程獲取)。
對象頭裏面的標記字段是一個可變的數據結構,如果是偏向鎖的話,標記字段裏面會有一個ThreadId字段。當線程第一次獲取鎖的時候直接把自己的threadId賦值給標記字段的threadId並且把當前鎖標記爲偏向鎖。如果第二次獲取鎖的時候直接比較這個threadId是否一致,如果一致直接獲取鎖,如果不一致則膨脹成輕量級鎖。

3、輕量級鎖(雖然有多線程請求鎖,但是都是不同時段來請求的,並不存在鎖競爭的情況,這種情況使用輕量級鎖)。
會在當前線程的棧幀中開闢一塊空間作爲該鎖的記錄,然後通過CAS將鎖對象的標記字段拷入這塊空間,並將owner字段直接指向標記字段,此時獲得到鎖。當程序再次進入同步代碼的時候會判斷當前的標記字段裏面的指針是否指向棧幀,如果是則直接獲取鎖,不是則變成重量級鎖。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章