synchronized底層原理以及鎖升級過程

概念:

synchronized是Java提供的一個併發控制的關鍵字,作用於對象上。主要有兩種用法,分別是同步方法(訪問對象和clss對象)和同步代碼塊(需要加入對象),保證了代碼的原子性和可見性以及有序性,但是不會處理重排序以及代碼優化的過程,但是在一個線程中執行肯定是有序的,因此是有序的。

synchronized 的特性

1 原子性

所謂原子性就是指一個操作或者多個操作,要麼全部執行並且執行的過程不會被任何因素打斷,要麼就都不執行

被synchronized修飾的類或對象的所有操作都是原子的,因爲在執行操作之前必須先獲得類或對象的鎖,直到執行完才能釋放,這中間的過程無法被中斷(除了已經廢棄的stop()方法),即保證了原子性。

2 可見性

可見性是指多個線程訪問一個資源時,該資源的狀態、值信息等對於其他線程都是可見的

synchronized對一個類或對象加鎖時,一個線程如果要訪問該類或對象必須先獲得它的鎖,而這個鎖的狀態對於其他任何線程都是可見的,並且在釋放鎖之前會將對變量的修改刷新到主存當中,保證資源變量的可見性,如果某個線程佔用了該鎖,其他線程就必須在鎖池中等待鎖的釋放。

3 有序性

有序性值程序執行的順序按照代碼先後執行

Java允許編譯器和處理器對指令進行重排,但是指令重排並不會影響單線程的順序,它影響的是多線程併發執行的順序性。synchronized保證了每個時刻都只有一個線程訪問同步代碼塊,也就確定了線程執行同步代碼塊是分先後順序的,保證了有序性。

4 可重入性

當一個線程試圖操作一個由其他線程持有的對象鎖的臨界資源時,將會處於阻塞狀態,但當一個線程再次請求自己持有對象鎖的臨界資源時,這種情況屬於重入鎖。通俗一點講就是說一個線程擁有了鎖仍然還可以重複申請鎖。

synchronized的使用場景

一般歸結爲三種:

1.修飾靜態方法,給當前類對象加鎖,進入同步方法時需要獲得類對象的鎖

public class SynchronizedStaticFunDemo {
    private static int count;

    public static synchronized void addCount(){
        count = count + 1;
    }
}

2.修飾實例方法,給當前實例變量加鎖,進入同步方法時需要獲得當前實例的鎖

public class SynchronizedMemberDemo {
    private static int count;

    public synchronized void addCount(){
        count = count + 1;
    }
}

3.修飾同步方法塊,指定加鎖對象(可以是實例對象,也可以是類變量),對給定對象加鎖,進入同步方法塊時需要獲得加鎖對象的鎖

public class SynchronizedCodeBlockDemo {
    private static int count;

    public  void addCount(){
        synchronized (this){
            count = count + 1;
        }
    }
}

或者

public class SynchronizedCodeBlockDemo {
    private static int count;

    public  void addCount(){
        synchronized (SynchronizedCodeBlockDemo.class){
            count = count + 1;
        }
    }
}

synchronized對鎖的底層實現

在理解鎖實現原理之前先了解一下Java的對象頭和Monitor,首先我們得先了解java對象結構,在對象頭裏面的Mark word中, 存儲對象的hashCode、鎖信息或分代年齡或GC標誌等信息。

Monitor

什麼是Monitor?

1.Monitor是一種用來實現同步的工具

2.與每個java對象相關聯,所有的 Java 對象是天生攜帶 monitor

3.Monitor是實現Sychronized(內置鎖)的基礎

對象的監視器(monitor)由ObjectMonitor對象實現(C++),其跟同步相關的數據結構如下:

ObjectMonitor() {
    _count        = 0; //用來記錄該對象被線程獲取鎖的次數
    _waiters      = 0;
    _recursions   = 0; //鎖的重入次數
    _owner        = NULL; //指向持有ObjectMonitor對象的線程 
    _WaitSet      = NULL; //處於wait狀態的線程,會被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _EntryList    = NULL ; //處於等待鎖block狀態的線程,會被加入到該列表
}

下面通過兩個列子:

代碼:

public class SynchronizedDemo {
    public void method(){
        synchronized (this){
            System.out.println("Method 1 start");
        }
    }
}

反編譯查看如圖:

在這裏插入圖片描述

通過上面代碼看

monitorenter:

每個對象有一個監視器鎖(monitor)。當monitor被佔用時就會處於鎖定狀態,線程執行monitorenter指令時嘗試獲取monitor的所有權,過程:

1、如果monitor的進入數爲0,則該線程進入monitor,然後將進入數設置爲1,該線程即爲monitor的所有者。

2、如果線程已經佔有該monitor,只是重新進入,則進入monitor的進入數加1.

3.如果其他線程已經佔用了monitor,則該線程進入阻塞狀態,直到monitor的進入數爲0,再重新嘗試獲取monitor的所有權。

monitorexit:

1.執行monitorexit的線程必須是objectref所對應的monitor的所有者。

2.指令執行時,monitor的進入數減1,如果減1後進入數爲0,那線程退出monitor,不再是這個monitor的所有者。其他被這個monitor阻塞的線程可以嘗試去獲取這個 monitor 的所有權。

代碼修改下:

public class SynchronizedDemo {
    public synchronized void method(){
            System.out.println("Method 1 start");
    }
}

反編譯如圖:
在這裏插入圖片描述
從反編譯的結果來看,其常量池中多了ACC_SYNCHRONIZED標示符,JVM就是根據該標示符來實現方法的同步的:當方法調用時,調用指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標誌是否被設置,如果設置了,執行線程將先獲取monitor,獲取成功之後才能執行方法體,方法執行完後再釋放monitor。

總結

Synchronized的底層是通過一個monitor的對象來完成,wait/notify等方法也依賴於monitor對象,這就是爲什麼只有在同步的塊或者方法中才能調用wait/notify等方法,否則會拋出java.lang.IllegalMonitorStateException的異常的原因。

Synchronized 鎖升級過程

鎖也分不同狀態,JDK6之前只有兩個狀態:無鎖、有鎖(重量級鎖),而在JDK6之後對synchronized進行了優化,新增了兩種狀態,總共就是四個狀態:無鎖狀態、偏向鎖、輕量級鎖、重量級鎖,其中無鎖就是一種狀態了。鎖的類型和狀態在對象頭Mark Word中都有記錄,在申請鎖、鎖升級等過程中JVM都需要讀取對象的Mark Word數據。

synchronized同步鎖升級步驟是:

偏向鎖 -> 輕量級鎖 -> 重量級鎖

偏向鎖

偏向鎖他會偏向第一次訪問的線程,當線程獲取鎖對象時,會在java對象頭markword中記錄偏向鎖的threadID,並不會主動釋放偏向鎖。當同一個線程再次獲取鎖時會比較當前的threadID與對象頭中的threadID是否一致。如果一致則不需要通過CAS來加鎖、解鎖。如果不一致並且線程還需要持續持有鎖,則暫停當前線程撤銷偏向鎖,升級爲輕量級鎖。如果不在需要持續持有鎖則鎖對象頭設爲無鎖狀態,重新設置偏向鎖。

偏向鎖過程:

  1. 訪問Mark Word中偏向鎖的標識是否設置成1,鎖標識位是否爲01,確認偏向狀態
  2. 如果爲可偏向狀態,則判斷當前線程ID是否爲偏向線程
  3. 如果偏向線程未只想當前線程,則通過cas操作競爭鎖,如果競爭成功則操作Mark Word中線程ID設置爲當前線程ID
  4. 如果cas偏向鎖獲取失敗,則掛起當前偏向鎖線程,偏向鎖升級爲輕量級鎖。

輕量級鎖(自旋鎖)

輕量級鎖由偏向鎖升級而來,偏向鎖運行在一個線程同步塊時,第二個線程加入鎖競爭的時候,偏向鎖就會升級爲輕量級鎖。

輕量級鎖過程:

  1. 線程由偏向鎖升級爲輕量級鎖時,會先把鎖的對象頭MarkWord複製一份到線程的棧幀中,建立一個名爲鎖記錄空間(Lock Record),用於存儲當前Mark Word的拷貝。
  2. 虛擬機使用cas操作嘗試將對象的Mark Word指向Lock Record的指針,並將Lock record裏的owner指針指對象的Mark Word。
  3. 如果cas操作成功,則該線程擁有了對象的輕量級鎖。第二個線程cas自選鎖等待鎖線程釋放鎖。
  4. 如果多個線程競爭鎖,輕量級鎖要膨脹爲重量級鎖,Mark Word中存儲的就是指向重量級鎖(互斥量)的指針。其他等待線程進入阻塞狀態。

重量級鎖

自旋失敗,很大概率 再一次自旋也是失敗,因此直接升級成重量級鎖,進行線程阻塞,減少cpu消耗。

當鎖升級爲重量級鎖後,未搶到鎖的線程都會被阻塞,進入阻塞隊列。

synchronized的執行過程

  1. 檢測Mark Word裏面是不是當前線程的ID,如果是,表示當前線程處於偏向鎖
  2. 如果不是,則使用CAS將當前線程的ID替換Mard Word,如果成功則表示當前線程獲得偏向鎖,置偏向標誌位1
  3. 如果失敗,則說明發生競爭,撤銷偏向鎖,進而升級爲輕量級鎖。
  4. 當前線程使用CAS將對象頭的Mark Word替換爲鎖記錄指針,如果成功,當前線程獲得鎖
  5. 如果失敗,表示其他線程競爭鎖,當前線程便嘗試使用自旋來獲取鎖。
  6. 如果自旋成功則依然處於輕量級狀態。
  7. 如果自旋失敗,則升級爲重量級鎖。

用戶態和內核態

  • 內核態

CPU可以訪問內存所有數據,包括外圍設備,例如硬盤,網卡。CPU也可以將自己從一個程序切換到另一個程序

  • 用戶態

只能受限的訪問內存,切不允許訪問外圍設備。佔用CPU的能力被剝奪,CPU資源可以被其他程序獲取。

之所以會有這樣的卻分是爲了防止用戶進程獲取別的程序的內存數據,,或者獲取外圍設備的數據。

用戶態和內核態什麼時候進行切換

用戶程序都是運行在用戶態的,但是有時候程序確實需要做一些內核的事情,例如從硬盤讀取數據,或者從硬盤獲取輸入,而唯一可以做這些事情的就是操作系統(synchronized中依賴的monitor也需要依賴操作系統完成,因此需要用戶態到內核態的切換)所以程序就需要先操作系統請求以程序的名義來執行這些操作。

總結

synchronized鎖升級實際上是把本來的悲觀鎖變成了 在一定條件下 使用無鎖(同樣線程獲取相同資源的偏向鎖),以及使用樂觀(自旋鎖 cas)和一定條件下悲觀(重量級鎖)的形式。

偏向鎖: 適用於單線程適用鎖的情況

輕量級鎖:適用於競爭較不激烈的情況(這和樂觀鎖的使用範圍類似)

重量級鎖:適用於競爭激烈的情況

如果大家對java架構相關感興趣,可以關注下面公衆號,會持續更新java基礎面試題, netty, spring boot,spring cloud等系列文章,一系列乾貨隨時送達, 超神之路從此展開, BTAJ不再是夢想!

架構殿堂

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