談談Synchronized底層及其優化

Synchronized的引出

同步問題的引出:由於多個線程對共享資源的操作而導致的同步問題

  • Synchronized是JDK1.0提供的一種同步手段,來處理同步問題

Synchronized保證了可見性與原子性

可見性:確保在鎖被釋放之前,對共享變量所做的修改,對於隨後獲得該鎖的另一個線程可見(即獲得鎖就同時獲得了最新共享變量的值)

原子性:保證在臨界區中,只有一個線程去操控修改共享變量

Synchronized關鍵字處理有兩種模式:

  • 同步代碼塊
  • 同步方法

獲取Sychronized鎖的方式:

  • 獲取對象鎖
  • 獲取類鎖

注意:獲取類鎖的本質也是獲取對象鎖,相當於獲取class對象鎖,它的所有類實例共享一個類

關於對象鎖與類鎖:

  • 當有線程訪問對象的同步代碼塊時,其餘線程可以訪問該對象的非同步代碼塊。
  • 若鎖的是同一個對象時,一個線程在訪問對象的同步代碼塊時,另一個訪問對象的同步代碼塊的線程會被阻塞
  • 若鎖的是同一個對象時,一個線程在訪問對象的同步方法時,另一個訪問對象的同步方法的線程會被阻塞
  • 若鎖的是同一個對象時,一個線程在訪問對象的同步代碼塊時,另一個訪問對象的同步方法的線程會被阻塞,反之成立
  • 同一個類的不同對象鎖互不干擾
  • 類鎖是一種特殊的對象鎖,與上述基本一致。由於一個類只能有一把對象鎖,所以同一個類的不同對象使用類鎖將會是同步的
  • 類鎖與對象鎖互不干擾

Synchronized的實現原理

對象鎖機制(monitor)

Synchronized修飾對象

public class Main {
    private static Object object= new Object();
    public static void main(String[] args) {
        synchronized (object){
            System.out.println("This is synchronized");
        }
    }
}

通過Javap -verbose查看字節碼如下(截取重要的一段):

 public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: getstatic     #2                  // Field object:Ljava/lang/Object;
         3: dup
         4: astore_1
         5: monitorenter      <----- Look
         6: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
         9: ldc           #4                  // String This is synchronized
        11: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/Str
ing;)V
        14: aload_1
        15: monitorexit       <----- Look
        16: goto          24
        19: astore_2
        20: aload_1
        21: monitorexit       <----- Look
        22: aload_2
        23: athrow
        24: return

根據上面會發現:

      執行同步代碼塊後首先要先執行monitorenter指令,退出的時候monitorexit指令。通過分析之後可以看出,使用Synchronized進行同步,其關鍵就是必須要對對象的監視器monitor進行獲取,當線程獲取monitor後才能繼續往下執行,否則就只能等待。而這個獲取的過程是互斥的,即同一時刻只有一個線程能夠獲取到monitor。不知道大家是否注意到上述字節碼中包含一個monitorenter指令以及多個monitorexit指令。這是因爲Java虛擬機需要確保所獲得的鎖在正常執行路徑,以及異常執行路徑上都能夠被解鎖,所以在Hotspot底層相當於加了一個try_catch異常處理。

下面我們再看看Hotspot底層源碼

  // initialize the monitor, exception the semaphore, all other fields
  // are simple integers or pointers
  ObjectMonitor() {
    _header       = NULL;
    _count        = 0;
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;  
    _WaitSet      = NULL;
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ;
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;
  }
  • WaitSet:等待隊列   EntryList:鎖池   用來保存ObjectMonitor對象列表
  • owner字段:是指向持有ObjectMonitor的線程,當多個線程同時進入到同步列表時會先進入到EntryList中,當線程獲取到對象的monitor之後就會進入到object區域並把owner指向當前線程,同時計數器count會+1,若對象調用wait方法會釋放當前線程的monitor,onwer就會被恢復成null,count-1,並且該對象的實例就會被放入到WaitSet集合中等待被喚醒
  • count字段則是用來支持可重入

Monitor鎖的競爭,獲取與釋放:

Synchronized修飾同步方法

public class Main {
    public synchronized void foo(){
        System.out.println("Meth");
    }
}

同樣觀察字節碼:

public synchronized void foo();
    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 Meth
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
  • 我們會發現有一個ACC_SYNCHRONIZED標記,該標記表示在進入同步方法時,虛擬機要進行monitorenter操作,而當退出方法時不論是否正常返回都會執行monitorexit。這裏的monitorenter與monitorexit都是隱式的操作

 

Synchronized的優化

CAS操作(compare and swap)

由於在大多數情況下,鎖都會被一個線程去獲取,當沒必須要去阻塞其他線程後,這樣會導致頻繁的上下文切換,進而性能會降低很多,所以JDK1.5之後引入CAS來優化Synchronized。

  • 在使用Synchronized鎖時,相當於是一種悲觀鎖,在獲取到鎖的線程角度理解就是在任何時刻都會有別的線程會和我去競爭鎖,所以當一個線程獲取到鎖時就會阻塞其餘線程,鎖的粒過大。而CAS則是一種樂觀鎖策略(無鎖),表示在任何情況下都不會有其他線程與我去競爭鎖資源,既然沒有衝突自然不需要阻塞其他線程
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章