Synchronized筆記

用戶態和內核態

平時我們所寫的java程序是運行在用戶空間的,因爲我們的jvm對於操作系統來講就是一個普通程序。用戶空間的程序要執行讀寫硬盤、讀寫網絡、讀寫內存等重要操作時必須經過操作系統內核來進行。

在JDK早期,Synchronized是重量級鎖,每次申請鎖都需要調用系統內核。需要從用戶空間切換到內核空間,拿到鎖後再將狀態返回給用戶空間。

CAS

cas(compare and swap)它指的是比較與交換,使用無鎖的機制保證操作對象的原子性,說是無鎖,其實它是一個自旋鎖。

首先讀取當前值,在計算預期的結果值,在把值修改回去的時候,要比較一下原來讀出來的值和現在的值是否相等,如果相等,說明沒有別的線程改動過,更新爲新的值。如果不一樣則說明已經別的線程改過了,這個時候,再次讀取當前值,重新再來一遍。cas的底層是由彙編指令lock和cmpxchg(compare and exchange)組合在一起來支撐的。

ABA問題解決:使用version標記,每次操作version加1

java對象佈局

openjdk提供了一個查看java對象佈局的工具,在maven中引入如下依賴即可使用

        <dependency>
            <groupId>org.openjdk.jol</groupId>
            <artifactId>jol-core</artifactId>
            <version>0.10</version>
        </dependency>

當我們new 了一個java對象的時候,它在jvm裏面一定是佔據了一塊內存的,那麼這個java對象在內存中的佈局是什麼樣子的?

一個普通對象在內存中可以分成4個部分

  • markword:對象頭標記字,存儲對象自身的運行時數據信息,如鎖狀態標誌,偏向線程ID等。
  • class pointer:存儲類元數據的指針,它指向一個對象到底屬於哪個類。例如Object.class。
  • instance data:對象自身真正有效數據存儲區域,存儲了各個字段的內容
  • padding:如果一個對象前面的三部分不能被8字節整除,補齊到能整除

下面我們用jol這個工具來看一下一個對象在內存中的佈局
測試demo:

public class MackWordTest {
    public static void main(String[] args) {
        Object obj = new Object();
        String print = ClassLayout.parseInstance(obj).toPrintable();
        System.out.println(print);
    }
}

運行結果如下:

前8個字節爲mark word,後面4個字節爲class pointer,然後是補齊。
給對象加上synchronized同步鎖之後,我們發現mark word發生了變化,說明鎖信息存在了mark word,給對象加鎖,其實就是修改mark word。

鎖升級

我們要探究鎖升級的過程,只需要看它mark word的變化過程就可以了

鎖狀態 25位 31位 1位 4bit 1bit偏向鎖位 2bit鎖標誌位
無鎖態(new) unused hashCode unused 分代年齡 0 0 1
鎖狀態 54位 2位 1位 4bit 1bit偏向鎖位 2bit鎖標誌位
偏向鎖 當前線程指針JAVA Thread Epoch unused 分代年齡 1 0 1
鎖狀態 62位 2bit鎖標誌位
輕量級鎖/自旋鎖 指向線程棧中Lock Record的指針 0 0
重量級鎖 指向重量級鎖的指針 1 0
GC標記信息 CMS過程用到的標記信息 1 1

鎖的狀態分成4中:無鎖、偏向鎖、輕量級鎖或自旋鎖、重量級鎖
怎麼區分鎖的狀態呢?看鎖的標誌位和偏向鎖標誌位就好了
首先看鎖的標誌位:
00代表輕量級鎖,10代表重量級鎖,我們看到無鎖態和偏向鎖的鎖標誌位都是01,這個時候再看偏向鎖標誌位,也就是看後三位,001是無鎖,101是偏向鎖。
表中當前線程指針JAVA Thread指向的就是當前線程ID
Lock Record:
由於synchronized默認是可以重入的,每個線程上自旋鎖的時候,在自己的線程棧中生成一個LockRecord對象,哪個線程能在markword裏寫入自己LockRecord對象的指針,就算是持有了鎖。鎖重入的時候再次生成一個LockRecord,這樣就記錄了到底鎖了多少次。

重量級鎖的底層實現就是ObjectMonitor(om),ObjectMonitor裏面記錄了等待的線程和參與競爭的線程

一個java對象剛new出來的時候,它有可能是無鎖態或者匿名偏向狀態,這個時候我們再用synchronized(obj)給這個對象上鎖,優先上偏向鎖。

偏向鎖就是說它偏向於某個線程,因爲我們在日常使用鎖的時候,大多數都是在一個線程,爲了這一個線程要去調用系統內核kernel,太浪費資源了,所以JDK在這裏進行了優化。凡是有一個線程第一次獲得這把鎖,就認爲這把鎖偏向於它,也就是不驚動操作系統內核,只需要將線程ID放入mark word裏就可以了。

當我們有多個線程競爭同一個資源的時候,鎖升級爲輕量級鎖/自旋鎖

自旋鎖就是多個線程去競爭同一把鎖,通過CAS的方式,哪個線程能把自己的信息寫入mark word,就算是持有了鎖。自旋鎖也不需要調用操作系統內核。

如果有特別多的線程同時去競爭一把鎖,那這個時候自旋鎖就會出現問題,大家想一下,我們的自旋鎖是一直在做while循環,假設有10000個線程參與競爭,這個時候真正在幹活的只有1個線程,剩下的9999個線程都在做while自旋,cpu資源全都浪費了。所以這種情況下會升級成爲重量級鎖

重量級鎖跟輕量級鎖最大的區別在於,重量級鎖經過操作系統內核的調度之後,系統內核提供一把鎖的同時,還會爲鎖提供wait set隊列,這些獲取不到鎖的線程都進入隊列等待,什麼時候獲取到鎖,線程才能繼續執行。

這裏面重量級鎖需要經過系統內核kernel,而偏向鎖和輕量級鎖在用戶空間就可以完成。

下面我們來看一些細節,輕量級鎖在什麼情況下會升級成爲重量級鎖?

JDK1.6之前,輕量級鎖升級成重量級鎖有兩個條件:

  • 輕量級鎖自旋次數超過10次
  • 等待線程的數量超過cpu核數的2分之一

JDK1.6之後對此進行了優化,引入了自適應自旋,JDK會根據每個線程的運行情況來判斷是不是要升級。

從上圖中可以看到,偏向鎖沒有啓動的時候,我們new了一個對象,它是一個普通對象,偏向鎖已經啓動的時候,new出來是一個匿名偏向對象。這裏到底是什麼意思?

java提供了偏向鎖啓動配置的參數(使用java -XX:+PrintFlagsFinal可以查看JVM可設置的參數):
偏向鎖啓動延遲時間: -XX:BiasedLockingStartupDelay=4000
偏向鎖開關:-XX:UseBiasedLocking=true
偏向鎖默認是打開的,它有一個啓動延遲,默認是4秒鐘。

爲什麼偏向鎖要延遲4秒?

jvm在啓動過程中是有大量的線程競爭資源的,這個時候啓動偏向鎖是沒有意義的,所以延遲4秒,等待JVM啓動。

偏向鎖已經啓動,剛new的一個對象,還沒有任何線程持有這把鎖,這個時候沒有偏向任何線程,所以是匿名偏向

那如果是普通對象,在偏向鎖還沒啓動的時候,如果有競爭,這個時候就之間升級成輕量級鎖。

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