synchronized
-
由對象頭中的 對象標誌 根據鎖標誌位的不同而被複用 以及鎖升級策略
-
能用無鎖 就不要用鎖,能鎖代碼塊 就不鎖整個方法, 能用對象鎖 就不用類鎖. 儘可能讓鎖的粒度更小,以提高併發效率
-
每個對象\類 都是一把鎖, 底層是Monitor鎖
-
本質是依賴於操作系統的Mutex Lock實現,操作系統實現線程之間切換 需要從用戶態到內核態, 成本高
-
Monitor監視器是由ObjectMonitor實現的, 底層是C++ 的ObjectMonitor.hpp
-
Monitor 與 java對象如何關聯的
- 1、如果一個java對象被某個線程鎖住,則該java對象的mark word 字段中的LockWard指向monitor的起始地址
- 2、monitor的owner字段存放擁有 關聯對象鎖的線程id
-
用法
- 同步塊
- 同步方法
synchronized 底層演變
-
java的線程
- 調用start方法啓動一個線程,實際上是映射到操作系統原生線程之上的, 如果要阻塞或喚醒一個線程就需要操作系統的介入, 需要在用戶態和核心態之間切換, 這種切換會消耗大量的系統資源, 因爲用戶態和內核態都有各自專用的內存空間
-
jdk5以前,synchronized屬於重量級鎖,效率低下,因爲監視器(monitor)是依賴於底層 操作系統的Mutex Lock(系統互斥量) 來實現的, 掛起線程和恢復線程都需要轉入內核態去完成. 阻塞和喚醒一個線程需要操作系統切換CPU狀態完成, 這種狀態的切換需要耗費處理器時間,如果同步代碼塊中內容過於簡單, 這種切換的時間可能比用戶代碼執行時間還長, 時間成本高
-
Mutex Lock
- Monitor是在jvm底層實現的,底層代碼是c++。本質是依賴於底層操作系統的Mutex Lock實現,操作系統實現線程之間的切換需要從用戶態到內核態的轉換,狀態轉換需要耗費很多的處理器時間成本非常高。所以synchronized是Java語言中的一個重量級操作
-
jdk6之後, 爲了減少獲得鎖和釋放鎖所帶來的性能消耗, 引入了輕量級鎖和偏向鎖, 需要有個逐步升級的過程,別一開始就直接到重量級鎖
鎖介紹
-
無鎖態
- 只有一個線程,無競爭
-
偏向鎖
-
當一段同步代碼一直被同一個線程多次訪問,由於只有一個線程, 那麼該線程在後續訪問時便會自動獲取鎖
-
由來
- 實際應用運行過程中發現,“鎖總是同一個線程持有,很少發生競爭”,也就是說鎖總是被第一個佔用他的線程擁有,這個線程就是鎖的偏向線程
-
理論
- 在鎖第一次被擁有的時候,記錄下偏向線程ID。這樣偏向線程就一直持有着鎖(後續這個線程進入和退出這段加了同步鎖的代碼塊時,不需要再次加鎖和釋放鎖。而是直接比較對象頭裏面是否存儲了指向當前線程的偏向鎖)。
- 如果相等表示偏向鎖是偏向於當前線程的,就不需要再嘗試獲得鎖了,直到競爭發生才釋放鎖。以後每次同步,檢查鎖的偏向線程ID與當前線程ID是否一致,如果一致直接進入同步。無需每次加鎖解鎖都去CAS更新對象頭。如果自始至終使用鎖的線程只有一個,很明顯偏向鎖幾乎沒有額外開銷,性能極高。
- 假如不一致意味着發生了競爭,鎖已經不是總是偏向於同一個線程了,這時候可能需要升級變爲輕量級鎖,才能保證線程間公平競爭鎖。偏向鎖只有遇到其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程纔會釋放鎖,線程是不會主動釋放偏向鎖的
-
一個synchronized方法被一個線程搶到了鎖時,那這個方法所在的對象就會在其所在的Mark Word中將偏向鎖修改狀態位,同時還會有佔用前54位來存儲線程指針作爲標識。若該線程再次訪問同一個synchronized方法時,該線程只需去對象頭的Mark Word 中去判斷一下是否有偏向鎖指向本身的ID,無需再進入Monitor去競爭對象了。
-
偏向鎖的相關參數
- 偏向鎖在JDK1.6之後是默認開啓的,但是啓動時間有延遲, 所以需要添加參數-XX:BiasedLockingStartupDelay=0,讓其在程序啓動時立刻啓動
- 開啓: -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
- 關閉: -XX:-UseBiasedLocking
-
-
輕量級鎖
-
本質就是自旋鎖, 有線程來參與鎖的競爭,但是獲取鎖的衝突時間極短
-
理論
- (1). 輕量級鎖是爲了在線程近乎交替執行同步塊時提高性能
- (2). 主要目的: 在沒有多線程競爭的前提下,通過CAS減少重量級鎖使用操作系統互斥量產生的性能消耗,說白了先自旋再阻塞
- (3). 升級時機:當關閉偏向鎖功能或多線程競爭偏向鎖會導致偏向鎖升級爲輕量級鎖
- (4). 假如線程A已經拿到鎖,這時線程B又來搶該對象的鎖,由於該對象的鎖已經被線程A拿到,當前該鎖已是偏向鎖了。
而線程B在爭搶時發現對象頭Mark Word中的線程ID不是線程B自己的線程ID(而是線程A),那線程B就會進行CAS操作希望能獲得鎖。
此時線程B操作中有兩種情況- 如果鎖獲取成功,直接替換Mark Word中的線程ID爲B自己的ID(A → B),重新偏向於其他線程(即將偏向鎖交給其他線程,相當於當前線程"被"釋放了鎖),該鎖會保持偏向鎖狀態,A線程Over,B線程上位
- 如果鎖獲取失敗,則偏向鎖升級爲輕量級鎖,此時輕量級鎖由原持有偏向鎖的線程持有,繼續執行其同步代碼,而正在競爭的線程B會進入自旋等待獲得該輕量級鎖。
- 如果鎖獲取成功,直接替換Mark Word中的線程ID爲B自己的ID(A → B),重新偏向於其他線程(即將偏向鎖交給其他線程,相當於當前線程"被"釋放了鎖),該鎖會保持偏向鎖狀態,A線程Over,B線程上位
-
-
重量級鎖
- monitor實現, 進入之前monitorenter ,之後 monitor
-
比較
-
Displaced MarkWord: JVM會爲每個線程在當前線程的棧楨中創建用於存儲鎖記錄的空間
說明 | 無鎖 | 偏向鎖 | 輕量級鎖 | 重量級鎖 | |
---|---|---|---|---|---|
加鎖 | - | 鎖標誌位 001 | 將當前線程ID記錄到MarkWord、鎖標誌位 101 | 一個線程獲取鎖 會把鎖的markWord複製到自己的Displaced MarkWord,然後其他線程嘗試用CAS將鎖的MarkWord替換爲指向鎖記錄的指針, 成功則獲取鎖,失敗則自旋 鎖標誌位 000 | - |
解鎖 | MarkWord 當前線程ID清除 | 釋放鎖時,CAS將Displaced MarkWord的內容複製回MarkWord | |||
markWord存儲 | 指向的是線程ID | 線程棧中Lock Record的指針 | 堆中的monitor對象的指針 |
鎖升級過程
-
無鎖 -> 偏向鎖 -> 輕量級鎖 -> 重量級鎖
-
輕量鎖 -> 重量級
- JDK6之前 CAS次數達到10次, 就會升級重量級鎖
- JDK6之後 自適應次數
- 線程如果自旋成功了,那下次自旋的最大次數會增加
- 很少成功,下次會減少自旋的次數
鎖消除
- 從JIT角度看相當於無視它,synchronized (o)不存在了,這個鎖對象並沒有被共用擴散到其它線程使用,極端的說就是根本沒有加這個鎖對象的底層機器碼,消除了鎖的使用
- 一個線程自己加鎖,沒必要
鎖粗化
- 假如方法中首尾相接,前後相鄰的都是同一個鎖對象,那JIT編譯器就會把這幾個synchronized塊合併成一個大塊,加粗加大範圍,一次申請鎖使用即可,避免次次的申請和釋放鎖,提升了性能
拓展信息
- jdk15已經去掉了偏向鎖, 原因是因爲維護成本較高, 費力不討好
- 如果在jdk15還想用到偏向鎖,可以手動設置jvm參數: -XX:+UseBiased
- 鎖升級後與hashcode的關係
-
無鎖狀態下, Mark Word中可以存儲對象的hash code值,當對象的hashcode方法被第一次調用時,JVM會生成對應的hashcode值 存儲到Mark Word中
-
對於偏向鎖,在線程獲取偏向鎖時, 會用ThreadId 和 epoch 覆蓋hash code所在的位置. 如果一個對象的hashCode方法已經被調用過了,這個對象不能被設置偏向鎖.
- 如果可以, 那MarkWord中的hashcode必然會被覆蓋,就會導致 兩次hashcode方法不一樣
-
升級成了輕量級鎖,JVM會在當前線程的棧楨中創建一個鎖記錄空間,用戶存儲鎖對象的MarkWord拷貝, 該拷貝中是含有hashcode的
-
升級成重量級鎖, MarkWord保存的是重量級鎖指針(Monitor對象指針), ObjectMonitor類中有字段記錄非加鎖狀態下的MarkWord, 鎖記錄釋放後會將信息寫會對象頭
-
特殊場景
- 當一個對象已經計算過hash code, 它就無法進入偏向鎖狀態, 會跳過偏向鎖,直接進入輕量級鎖
- 偏向鎖過程中遇到一致性hash請求, 立馬撤銷偏向模式, 膨脹爲重量級鎖
-