Java 同步原語 synchronized 剖析和鎖優化

[TOC]

概述

Java 在語法層面提供了 synchronized 關鍵字來實現多線程同步,雖然 Java 有 ReentrantLock 等高級鎖,但是 synchronized 用法簡單,不易出錯,并且 JDK6 對其進行了諸多優化,性能也不差,故而依然值得我們去使用。

本文,我們將對 synchronized 對實現進行剖析,分析其實現原理以及 JDK6 引入了哪些鎖優化對手段。

synchronized 實現

我們先看一段代碼:

public class LockTest {

    public synchronized void testSync() {
        System.out.println("testSync");
    }

    public void testSync2() {
        synchronized(this) {
            System.out.println("testSync2");
        }
    }
}

在這段代碼中,分布使用 synchronized 對方法和語句塊進行了同步,接下來我們使用 javac 編譯后,再用 javap 命令查看其匯編代碼:

javap -verbose LockTest.class

  public synchronized void testSync();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String testSync
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 9: 0
        line 10: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  this   Lcom/qunar/fresh2017/LockTest;

  public void testSync2();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter
         4: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         7: ldc           #5                  // String testSync2
         9: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        12: aload_1
        13: monitorexit
        14: goto          22
        17: astore_2
        18: aload_1
        19: monitorexit
        20: aload_2
        21: athrow
        22: return
      Exception table:
         from    to  target type
             4    14    17   any
            17    20    17   any
      LineNumberTable:
        line 13: 0
        line 14: 4
        line 15: 12
        line 16: 22
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      23     0  this   Lcom/qunar/fresh2017/LockTest;
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 17
          locals = [ class com/qunar/fresh2017/LockTest, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4

這里我們省略了關鍵方法之外對常量池、構造函數等部分,對于 synchronized 方法,僅僅在其 class 文件的 access_flags 字段中設置了 ACC_SYNCHRONIZED 標志。對于 synchronized 語句塊,分布在同步塊的入口和出口插入了 monitorenter 和 monitorexit 字節碼指令。

注意這里有多個 monitorexit ,除了在正常出口插入了 monitorexit,還在異常處理代碼里插入了 monitorexit(請看 Exception table )。

在這篇文章 OpenJDK9 Hotspot : synchronized 淺析里,可以看到 monitorenter 的處理邏輯位于 bytecodeInterpreter.cpp 中,其加鎖順序為偏向鎖 -> 輕量級鎖 -> 重量級鎖。最終重量級鎖的代碼位于 ObjectMonitor::enter 中,enter 調用了 EnterI 方法,EnterI 方法中,調用了線程擁有的 Parker 實例的 park 方法,這一點和 LockSupport 一致,畢竟都是需要底層操作系統支持的。

// in /vm/runtime/objectMonitor.cpp
void ATTR ObjectMonitor::enter(TRAPS) {
    // omit a lot
    for (;;) {
      jt->set_suspend_equivalent();
      // cleared by handle_special_suspend_equivalent_condition()
      // or java_suspend_self()

      EnterI (THREAD) ;
      // omit a lot
    }
}
void ATTR ObjectMonitor::EnterI (TRAPS) {
    Thread * Self = THREAD ;
    // 省略很多
    for (;;) {
        // omit a lot
       
        // park self
        if (_Responsible == Self || (SyncFlags & 1)) {
            TEVENT (Inflated enter - park TIMED) ;
            Self->_ParkEvent->park ((jlong) RecheckInterval) ;
            // Increase the RecheckInterval, but clamp the value.
            RecheckInterval *= 8 ;
            if (RecheckInterval > 1000) RecheckInterval = 1000 ;
        } else {
            TEVENT (Inflated enter - park UNTIMED) ;
            Self->_ParkEvent->park() ;
        }
    }
    // omit a lot   

鎖的粒度

鎖的粒度是一個很關鍵的問題,粒度的大小對于線程的并發性能有很大影響,比如數據庫中表鎖的并發度要比遠低于行鎖。下面介紹一下 synchronized 幾種常見用法中,鎖的粒度:

  1. 對于同步方法,鎖是當前實例對象。
  2. 對于靜態同步方法,鎖是當前對象的Class對象。
  3. 對于同步方法塊,鎖是 synchronized 括號里配置的對象。

鎖優化

JVM

JDK6 中為了提升鎖的性能,引入了“偏向鎖”和“輕量級鎖”的概念,所以在 Java 中鎖一共有 4 種狀態:無鎖、偏向鎖、輕量級鎖、重量級鎖。它會隨著競爭情況逐漸升級,鎖可以升級但不能降級,也就是說不能有重量級鎖變為輕量級鎖,也不能由輕量級鎖變為偏向鎖。下面我們介紹一下這幾種鎖的特點:

場景 優點 缺點
偏向鎖 適用于只有一個線程訪問同步塊的場景 加鎖和解鎖不存在額外的消耗,和執行非同步方法比僅存在納秒級的差距 如果線程間存在競爭,會帶來額外的鎖撤銷的消耗
輕量級鎖 適用于同步塊執行速度非常快的場景 競爭線程不會阻塞,減少了線程切換消耗的時間 如果線程間競爭激烈,會導致過多的自旋,消耗 CPU
重量級鎖 使用于同步款執行較慢,鎖占用時間較長的場景 競爭激烈時,消耗 CPU 較少 線程阻塞,會引起線程切換

可以說,沒有哪一種鎖絕對比另一種鎖好,各自都有其適合的場景。下面再簡單描述一下,這些鎖具體是如何實現的:

  1. 偏向鎖:對象頭的鎖標志位置為 1 表示當前是偏向鎖,另有 23bit 用來記錄當前線程 id。如果一個線程發現當前是無鎖狀態,會將鎖狀態改為偏向鎖。如果已經是偏向鎖,并且記錄的線程 id 和當前線程一致,則認為是獲得了鎖;否則升級鎖到輕量級鎖。
  2. 輕量級鎖:其本質是自旋鎖,也就是輪詢去獲取鎖,實際中功能會高級一點兒,有自適應自旋功能,所謂自適應也就是根據競爭激烈程度適當調整自旋次數,以及決定是否升級為重量級鎖。
  3. 重量級鎖:其底層實現通常就是 POSIX 中的 mutex 和 condition。

代碼

前面講了 JVM 中優化鎖的性能的一些方法,這里再擴展一下,在實際的代碼編寫過程中,使用鎖時,有哪些方法可以改進性能?

  1. 減少鎖占用時間:減少鎖占用時間,能過有效的減少競爭激烈程度,減少到一定程度,就可以使用輕量級鎖,代替重量級鎖來提升性能。如何減少呢?
  • 減少不必要的占用鎖的代碼。
  • 降低鎖的粒度。
  1. 鎖分離:該技術能過降低鎖占用時間,或者減少競爭激烈程度。
  • 將一個大鎖分解多個小鎖,比如從 HashTable 的對象級別鎖到 ConcurrentHashMap 的分段鎖的優化。
  • 按照同步操作的性質來拆分,比如讀寫鎖大大提升了讀操作的性能。
  1. 鎖粗化:看起來與鎖分離相反,但是它們適用的場景不同。在一個間隔性地需要執行同步語句的線程中,如果在不連續的同步塊間頻繁加鎖解鎖是很耗性能的,因此把加鎖范圍擴大,把這些不連續的同步語句進行一次性加鎖解鎖。雖然線程持有鎖的時間增加了,但是總體來說是優化了的。
  2. 鎖消除:根據代碼逃逸技術的分析,如果一段代碼中的數據不會逃逸出當前線程,那么可以認為這段代碼是線程安全的,不必加鎖。

總結

在實際中,鎖的各種優化技術是可以一起使用的。比如在減少鎖占用時間后,就可以使用自旋鎖代替重量級鎖提升性能。

參考資料

  1. OpenJDK9 Hotspot : synchronized 淺析
  2. Java SE1.6 中的 Synchronized
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容