多線程原理分析 -- synchronized (超詳細)

多線程原理分析(一)

一、包含的知識點

  • 多線程帶來的問題(好處、壞處)
  • synchronized加鎖方式
  • 鎖信息是如何存儲的(Header、Mark Word)
  • synchronized鎖升級流程(偏向鎖、輕量級鎖、重量級鎖)

二、多線程帶來的問題

2.1 多線程帶來的好處、壞處

  • 能夠充分利用多核CPU, 實現線程並行執行
  • 多線程對於共享變量訪問帶來安全性問題

2.2 線程安全問題的本質

​ 線程安全性本質,是對於數據狀態的訪問, 這個狀態通常是共享的、可變的, 而問題的本質是對共享變量的訪問

  • 共享的, 指數據變量可以被多個線程訪問
  • 可變的, 指數據變量在生命週期內是可改變的

2.3 解決線程安全問題方式

  • synchronized

三、Synchronized基本認識

3.1 synchronized 加鎖方式

  • 修飾實例方法, 對當前實例加鎖,進入同步代碼前要獲得當前**實例的鎖**
  • 修飾代碼塊, 對當前類對象加鎖,進入同步代碼前要獲得當前**類對象的鎖**
  • 修飾類方法, 指定加鎖對象,對給定對象加鎖,進入同步代碼庫前要獲得**給定對象的鎖**

這裏以代碼塊加鎖進行舉例

public class SynchronizedThread {
    private static int count = 0 ;

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0 ; i < 2000; i++) {
            new Thread(() -> incr()).start();
        }

        Thread.sleep(3000);
        System.out.println("result -> " + count);
    }

    public static void incr() {
        synchronized (SynchronizedThread.class) {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            count++ ;
        }
    }
}

3.2 鎖是如何存儲的

3.2.1 鎖包含的數據內容

​ 這裏以 synchronized(lock) 爲例來說明

​ 同步代碼塊以lock的生命週期來控制鎖的粒度, 對象在內存中的佈局主要包含三部分: 對象頭(Header)、實例數據(InstanceData)、對其填充(Padding)

​ 下面是數據結構關係圖, 鎖的信息就維護在對象頭(Header)中

在這裏插入圖片描述

3.2.2 JVM創建對象實現

​ 創建Java對象時, JVM使用 InstanceOopDesc和arrayOopDesc 來描述對象頭, 下面是InstanceOop.hpp文件的內容

#ifndef SHARE_VM_OOPS_INSTANCEOOP_HPP
#define SHARE_VM_OOPS_INSTANCEOOP_HPP

#include "oops/oop.hpp"

// An instanceOop is an instance of a Java Class
// Evaluating "new HashTable()" will create an instanceOop.

class instanceOopDesc : public oopDesc {
 public:
  // aligned header size.
  static int header_size() { return sizeof(instanceOopDesc)/HeapWordSize; }

  // If compressed, the offset of the fields of the instance may not be aligned.
  static int base_offset_in_bytes() {
    // offset computation code breaks if UseCompressedClassPointers
    // only is true
    return (UseCompressedOops && UseCompressedClassPointers) ?
             klass_gap_offset_in_bytes() :
             sizeof(instanceOopDesc);
  }

  static bool contains_field_offset(int offset, int nonstatic_field_size) {
    int base_in_bytes = base_offset_in_bytes();
    return (offset >= base_in_bytes &&
            (offset-base_in_bytes) < nonstatic_field_size * heapOopSize);
  }
};

#endif // SHARE_VM_OOPS_INSTANCEOOP_HPP

​ 文中沒有我們想了解的_mark、_medaData信息, 而InstanceOopDesc 繼承了oopDesc , 查看oop.hpp文件內容, 包含下面的內容

class oopDesc {
 private:
  volatile markOop  _mark; //對象標記信息
  union _metadata {
    Klass*      _klass;
    narrowKlass _compressed_klass;
  } _metadata; //類元信息
}

​ 對象標記信息類型屬於markOop, 查看markOop.hpp文件, 包含下面的內容

//  32 bits:
//  --------
//             hash:25 ------------>| age:4    biased_lock:1 lock:2 (normal object)
//             JavaThread*:23 epoch:2 age:4    biased_lock:1 lock:2 (biased object)
//             size:32 ------------------------------------------>| (CMS free block)
//             PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)

enum { age_bits                 	= 4, // 分代年齡
         lock_bits                = 2, // 鎖標誌 0-false, 1-true
         biased_lock_bits         = 1, // 是否是偏向鎖標誌
      	 //hashcode
         max_hash_bits            = BitsPerWord - age_bits - lock_bits - biased_lock_bits,
         hash_bits                = max_hash_bits > 31 ? 31 : max_hash_bits,
         cms_bits                 = LP64_ONLY(1) NOT_LP64(0),
         epoch_bits               = 2  //偏向鎖時間戳
  };

​ markOpp存儲的數據會隨着鎖標誌位的變化而變化, 包含的數據對象存在下面的情況

鎖狀態 25bit 4bit 1bit 2bit
23bit 2bit 是否偏向鎖 鎖標誌位
無鎖 對象的hashcode 分代年齡 0 01
偏向鎖 線程ID Epoch 分代年齡 1 01
輕量級鎖 指向棧中鎖記錄的指針 00
重量級鎖 指向互斥量(重量級鎖)的指針 10
GC標誌 11

3.2.3 爲什麼任何對象都可以實現鎖

  • Java中每個對象都隱式的繼承Object, 而Object對象在JVM中都有一個native對象oop/oopDesc與之對應

  • 線程獲取鎖, 實際獲取的是監視器對象(monitor), 多線程訪問同步代碼, 相當於爭搶監視器對象, 修改對象鎖標誌

    而Java對象天生帶有monitor對象,下面是markOop.hpp文件monitor

      ObjectMonitor* monitor() const {
        assert(has_monitor(), "check");
        // Use xor instead of &~ to provide one extra tag-bit check.
        return (ObjectMonitor*) (value() ^ monitor_value);
      }
    

3.3 synchronized鎖升級

​ 在分析鎖如何存儲一節中, markOop對鎖進行了分類, 按照鎖的狀態分爲: 無鎖、偏向鎖、輕量級鎖、重量級鎖

​ 在無鎖的情況下, 對共享變量訪問性能比較高, 但是高併發情況下存在安全問題 ; 加鎖情況下, 可以保證共享變量安全問題, 但是性能比較低, 爲了在數據訪問安全性和數據訪問性能之間有個折中, 提出了偏向鎖、輕量級鎖的概念(JDK6提供)

3.3.1 偏向鎖原理

​ 當一個線程訪問加了同步鎖的代碼塊時, 會在對象頭(Header)記錄下這個線程Id(ThreadId), 後續線程進入代碼時, 會比較線程ID是否相同, 如果相同, 說明是同一線程, 進入或者退出代碼塊時不用再次獲取/釋放鎖

​ 偏向, 表示偏向當前線程, 不用再次獲取線程鎖。

3.3.2 偏向鎖獲取/釋放邏輯

​ 在進行偏向鎖獲取/釋放邏輯講解前, 請先看下下面流程圖, 我們將以下面的流程圖來進行講解

在這裏插入圖片描述

偏向鎖獲取流程

  • 首相獲取鎖對象的Mark Word, 判斷是否處於可偏向狀態(biased_lock=1, 且ThreadId爲空)
  • 如果是可偏向狀態, 通過CAS方式將ThreadId寫如鎖對象頭(Header)中
    • 如果CAS成功, 表示成功獲取了鎖對象的偏向所, 接着執行同步代碼塊
    • 如果CAS失敗, 表示有其它線程獲取了鎖對象的偏向鎖, 說明存在鎖衝突現象, 需要撤銷偏向鎖, 將鎖升級爲輕量級鎖(這個操作需要等到全局安全點)
  • 如果是已偏向狀態, 需要檢測Mark Word中存儲的線程id(ThreadId)是否和當前ThreadId一致
    • 如果相同, 不需要再次獲取鎖, 可以進入同步代碼塊進行邏輯操作
    • 如果不相同, 說明存在其它線程獲取了偏向鎖, 需要撤銷偏向鎖, 進行鎖升級到輕量級鎖

偏向鎖釋放流程

​ 因爲鎖只能單向升級, 不能降級, 這裏偏向鎖的釋放不是將對象恢復爲無鎖狀態, 而是CAS失敗時, 將鎖升級爲輕量級鎖, 對原持有偏向鎖的線程進行撤銷時, 存在下面的情況

  • 原持有偏向鎖的線程離開了臨界區, 這個時候會將對象頭(Header) 設置爲無鎖狀態,其它線程可以通過CAS進行鎖的獲取
  • 原持有偏向鎖的線程還在臨界區內, 即代碼塊還沒有執行完, 會將原持有偏向鎖的線程升級爲輕量級鎖繼續執行代碼塊

3.3.3 輕量級鎖獲取/釋放邏輯

​ 在進行輕量級鎖獲取/釋放邏輯講解前, 請先看下下面流程圖, 我們將以下面的流程圖來進行講解

[

輕量級鎖獲取流程

​ 輕量級鎖獲取是通過自旋鎖方式實現的。自旋 , 是指當其它線程進行鎖競爭時, 這個線程不會立即進入阻塞狀態, 而是原地循環等待, 在原持有鎖的線程釋放鎖之後,這個線程會立即獲取鎖。

​ 自旋的過程會消耗CPU, 自旋需要考慮下面情況

  1. 輕量級鎖適合同步代碼塊處理時間很短的場景

  2. 需要控制自旋次數, 避免自旋時間過長, JDK6引入了自適應自旋鎖概念, 它會根據前一次自旋時間來決定本次是否需要自旋, 避免資源浪費

​ 升級流程如下:

  1. 線程在自己的棧針中創建鎖記錄對象(LockRecord)

  2. 將鎖對象頭中MarkWord記錄,複製到創建的LockRecord中

  3. 將鎖對象的Owner指針指向鎖對象

  4. 將鎖對象的對象頭Mark Word 替換爲指向鎖記錄的指針

輕量級鎖釋放流程

​ 輕量級鎖釋放邏輯是獲取鎖的逆邏輯, 通過CAS操作把線程棧中的Lock Record替換到Mark Word中, 如果替換成功,表示沒有競爭, 如果失敗表示存在競爭, 需要對鎖進行升級操作, 將鎖升級爲重量級鎖 。

3.3.4 重量級鎖獲取/釋放邏輯

​ 進行重量級鎖講解前, 請先看下下面代碼及其執行情況(javap -v SynchronizedMonitor)

public class SynchronizedMonitor {

    public static void main(String[] args) {
        synchronized (SynchronizedMonitor.class) {

        }
        test() ;
    }

    public static void test() {
        System.out.println("Good");
    }
}
 public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: ldc           #2                  // class com/ityongman/thread/SynchronizedThread
         2: dup
         3: astore_1
         4: monitorenter
         5: aload_1
         6: monitorexit
         7: goto          15
        10: astore_2
        11: aload_1
        12: monitorexit
        13: aload_2
        14: athrow
        15: invokestatic  #3                  // Method test:()V
        18: return

​ 從字節碼可以看出, 線程進入/退出同步代碼塊是通過 monitorenter/monitorexit 控制, 線程想要執行被synchronized修飾的同步方法或者同步代碼塊, 必須要先獲得監視器對象(Monitor), 而每個Java對象都有一個Monitor和它關聯(詳細內容可以查看3.2節)

  • monitorenter, 表示獲取一個對象的監視器(monitor)
  • monitorexit, 表示釋放監視器對象(monitor)的所有權, 讓處於等待隊列中的線程可以重新競爭監視器鎖

​ monitor依賴操作系統的MutexLock(互斥鎖)來實現的, 線程被阻塞後會進入內核調用狀態,這個操作會導致用戶態和內核態相互切換, 而這個操作很影響性能。下面是重量級鎖加鎖流程

在這裏插入圖片描述

​ 任意線程對臨界資源的訪問, 首先要獲取lock的監視器對象, 如果獲取對象失敗, 線程會進入同步隊列, 線程狀態變爲(BLOCKED), 當持有監視器鎖的線程釋放了鎖, 會喚醒同步隊列中阻塞的線程, 重新嘗試獲取鎖資源。

NOTE: 線程獲取鎖實際獲取的是臨界資源監視器(monitor)鎖, Object中有wait() 、notify()、notifyAll()方法釋放鎖資源或通知其它線程重新競爭鎖資源, 這些方法的調用必須子synchronized修改的同步範圍內, 不然會拋下異常IllegalMonitorStateException, 意思是因爲沒有同步,線程對對象鎖的狀態是不確定的,不能調用這些方法 。

//一、拋出異常
public class SynchronizedThread extends Thread{
    public static void main(String[] args) throws InterruptedException {
        synchronized (SynchronizedThread.class) {
        }
        SynchronizedThread.class.wait();
    }
}

//二、無異常, 正常執行
public class SynchronizedThread extends Thread{
    public static void main(String[] args) throws InterruptedException {
        synchronized (SynchronizedThread.class) {
          	SynchronizedThread.class.wait();
        }   
    }
}
Exception in thread "main" java.lang.IllegalMonitorStateException
	at java.lang.Object.wait(Native Method)
	at java.lang.Object.wait(Object.java:502)
	at com.ityongman.thread.SynchronizedThread.main(SynchronizedThread.java:13)

3.4 鎖的優缺點對比

優點 缺點 適用場景
偏向鎖 加鎖和解鎖不需要額外的消耗,和執行非同步方法比僅存在納秒級的差距 如果線程間存在鎖競爭,會帶來額外的鎖撤銷的消耗 適用於只有一個線程訪問同步塊場景
輕量級鎖 競爭的線程不會阻塞,提高了程序的響應速度 如果始終得不到鎖競爭的線程使用自旋會消耗CPU 追求響應時間,同步塊執行速度非常快
重量級鎖 線程競爭不使用自旋,不會消耗CPU 線程阻塞,響應時間緩慢 追求吞吐量, 同步塊執行速度較長

參考信息:

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