你真的瞭解synchronized嗎?

在多線程併發編程中synchronized一直是元老級角色,不少人都會稱呼它爲重量級鎖。可是,隨着Java SE 1.6對synchronized進行了各類優化以後,有些狀況下它就並不那麼重了。本文詳細介紹java1.6中爲了減小 synchronized 獲取鎖和釋放鎖鎖帶來的嚴重的性能消耗而引入的偏向鎖和輕量級鎖,以及鎖膨脹的過程!java

1、synchronized實現鎖的表現形式

  1. 修飾實例方法,對於普通同步方法,鎖是當前的實例對象web

  2. 修飾靜態方法,對於靜態同步方法,鎖是當前的Class對象shell

  3. 修飾方法代碼塊,對於同步方法塊,鎖是synchronized括號裏面配置的對象!編程

當一個線程試圖訪問同步代碼塊的時候,就必須獲得鎖,完成後(或者出現異常),就必須釋放鎖。那麼鎖究竟存在什麼地方呢?咱們一塊來探究!數組

不過,相信,既然你們可以找到這篇文章,相信你們對他的使用早已了熟於心,咱們對於使用,以及爲何多線程狀況下,數據會出現錯亂狀況,不作詳細的解釋!只把他的幾種使用方式列出,供參考!安全

①修飾實例方法

修飾實例方法,對於普通同步方法,鎖是當前的實例對象微信

這個沒得說,使用的同一個實例,添加上synchronized後,線程須要排隊,完成一個原子操做,可是注意前提是使用的同一個實例,他纔會生效!多線程

正例:併發

/**
* @author huangfu
*/
public class ExploringSynchronized implements Runnable {
   /**
    * 共享資源(臨界資源)
    */
   static int i=0;
   public synchronized void add(){
       i++;
  }

   @Override
   public void run() {
       for (int j = 0; j < 100000; j++) {
           add();
      }
  }

   public static void main(String[] args) throws InterruptedException {
       ExploringSynchronized exploringSynchronized = new ExploringSynchronized();
       Thread t1 = new Thread(exploringSynchronized);
       Thread t2 = new Thread(exploringSynchronized);
       t1.start();
       t2.start();
       //join 主線程須要等待子線程完成後在結束
       t1.join();
       t2.join();
       System.out.println(i);

  }
}

反例:app

/**
* @author huangfu
*/
public class ExploringSynchronized implements Runnable {
   /**
    * 共享資源(臨界資源)
    */
   static int i=0;
   public synchronized void add(){
       i++;
  }

   @Override
   public void run() {
       for (int j = 0; j < 100000; j++) {
           add();
      }
  }

   public static void main(String[] args) throws InterruptedException {
       Thread t1 = new Thread(new ExploringSynchronized());
       Thread t2 = new Thread(new ExploringSynchronized());
       t1.start();
       t2.start();
       //join 主線程須要等待子線程完成後在結束
       t1.join();
       t2.join();
       System.out.println(i);

  }
}

這種,即便你在方法上加上了synchronized也無濟於事,由於,對於普通同步方法,鎖是當前的實例對象!實例對象都不同了,那麼他們之間的鎖天然也就不是同一個!

②修飾靜態方法

修飾靜態方法,對於靜態同步方法,鎖是當前的Class對象

從定義上能夠看出來,他的鎖是類對象,那麼也就是說,以上面那個類爲例:普通方法的鎖對象是 new ExploringSynchronized()而靜態方法對應的鎖對象是ExploringSynchronized.class因此對於靜態方法添加同步鎖,即便你從新建立一個實例,它拿到的鎖仍是同一個!

package com.byit.test;

/**
* @author huangfu
*/
public class ExploringSynchronized implements Runnable {
   /**
    * 共享資源(臨界資源)
    */
   static int i=0;
   public synchronized static void add(){
       i++;
  }

   @Override
   public void run() {
       for (int j = 0; j < 100000; j++) {
           add();
      }
  }

   public static void main(String[] args) throws InterruptedException {
       Thread t1 = new Thread(new ExploringSynchronized());
       Thread t2 = new Thread(new ExploringSynchronized());
       t1.start();
       t2.start();
       //join 主線程須要等待子線程完成後在結束
       t1.join();
       t2.join();
       System.out.println(i);

  }
}

固然,結果是咱們期待的  200000

③修飾方法代碼塊

修飾方法代碼塊,對於同步方法塊,鎖是synchronized括號裏面配置的對象!

package com.byit.test;

/**
* @author huangfu
*/
public class ExploringSynchronized implements Runnable {
   /**
    * 鎖標記
    */
   private static final String LOCK_MARK = "LOCK_MARK";
   /**
    * 共享資源(臨界資源)
    */
   static int i=0;
   public void add(){
       synchronized (LOCK_MARK){
           i++;
      }
  }

   @Override
   public void run() {
       for (int j = 0; j < 100000; j++) {
           add();
      }
  }

   public static void main(String[] args) throws InterruptedException {
       Thread t1 = new Thread(new ExploringSynchronized());
       Thread t2 = new Thread(new ExploringSynchronized());
       t1.start();
       t2.start();
       //join 主線程須要等待子線程完成後在結束
       t1.join();
       t2.join();
       System.out.println(i);

  }
}

對於同步代碼塊,括號裏面是什麼,鎖對象就是什麼,裏面可使用this  字符串  對象等等!

2、synchronized的底層實現

java中synchronized的實現是基於進入和退出的 Monitor對象實現的,不管是顯式同步(修飾代碼塊,有明確的monitorentermonitorexit指令)仍是隱式同步(修飾方法體)!

須要注意的是,只有修飾代碼塊的時候,纔是基於monitorentermonitorexit指令來實現的;修飾方法的時候,是經過另外一種方式實現的!我會放到後面去說!

在瞭解整個實現底層以前,我仍是但願你可以大體瞭解一下對象在內存中的結構詳情!

  • 實例變量:存放類的屬性數據信息,包括父類的屬性信息,若是是數組的實例部分還包括數組的長度,這部份內存按4字節對齊。

  • 填充數據:因爲虛擬機要求對象起始地址必須是8字節的整數倍。填充數據不是必須存在的,僅僅是爲了字節對齊,這點了解便可。

這兩個概念,咱們簡單理解就好!咱們今天並不去探究對象的構成原理!咱們着重探究一下對象頭,他對咱們理解鎖尤其重要!

通常而言,synchronized使用的鎖存在於對象頭裏面!若是是數組對象,則虛擬機使用3個字寬存儲對象,若是是非數組對象,則使用兩個字寬存儲對象頭!字虛擬機裏面1字寬等於4字節!主要結構是 Mark WordClass Metadata Address組成,結構以下:

虛擬機位數 頭對象結構 說明
32/64bit Mark Word 存儲對象的hashCode、鎖信息或分代年齡或GC標誌等信息
32/64bit Class Metadata Address 存儲到隊形類型數據的指針
32/64bit(數組) Aarray length 數組的長度

經過上述表格可以看出  鎖信息 存在於 Mark Word  內,那麼 Mark Word 內又是如何組成的呢?

鎖狀態 25bit 4bit 1bit是不是偏向鎖 2bit鎖標誌位
無鎖狀態 對象的hashcode 對象的分代年齡 0 01

在運行起見,mark Word 裏存儲的數據會隨着鎖的標誌位的變化而變化。mark Word可能變化爲存儲一下四種數據

Java SE 1.6爲了減小得到鎖和釋放鎖帶來的消耗,引入了偏向鎖輕量級鎖,從以前上來就是重量級鎖到1.6以後,鎖膨脹升級的優化,極大地提升了synchronized的效率;

鎖一共有4中狀態,級別從低到高:

這幾個狀態會隨着鎖的競爭,逐漸升級。鎖能夠升級,可是不能降級,其根本的緣由就是爲了提升獲取鎖和釋放鎖的效率!

那麼,synchronized是又如何保證的線程安全的呢?或許咱們須要從字節碼尋找答案!

package com.byit.test;

/**
* @author Administrator
*/
public class SynText {
   private static String A = "a";
   public int i ;

   public void add(){
       synchronized (A){
           i++;
      }

  }
}

反編譯的字節碼

Compiled from "SynText.java"
public class com.byit.test.SynText {
public int i;

public com.byit.test.SynText();
  Code:
      0: aload_0
      1: invokespecial #1                 // Method java/lang/Object."<init>":()V
      4: return

public void add();
  Code:
      0: getstatic     #2                 // Field A:Ljava/lang/String;
      3: dup
      4: astore_1
      5: monitorenter
      6: aload_0
      7: dup
      8: getfield     #3                 // Field i:I
    11: iconst_1
    12: iadd
    13: putfield     #3                 // Field i:I
    16: aload_1
    17: monitorexit
    18: goto         26
    21: astore_2
    22: aload_1
    23: monitorexit
    24: aload_2
    25: athrow
    26: return
  Exception table:
      from   to target type
          6   18   21   any
        21   24   21   any

static {};
  Code:
      0: ldc           #4                 // String a
      2: putstatic     #2                 // Field A:Ljava/lang/String;
      5: return
}

省去沒必要要的,簡化在簡化

   5: monitorenter
    ...
    17: monitorexit
    ...
    23: monitorexit

從字節碼中可知同步語句塊的實現使用的是monitorentermonitorexit指令,其中monitorenter指令指向同步代碼塊的開始位置,monitorexit指令則指明同步代碼塊的結束位置,當執行monitorenter指令的時候,線程將試圖獲取對象所所對應的monitor特權,當monitor的的計數器爲0的時候,線程就能夠獲取monitor,並將計數器設置爲1.去鎖成功!若是當前線程已經擁有monitor特權,則能夠直接進入方法(可重入鎖),計數器+1;若是其餘線程已經擁有了monitor特權,那麼本縣城將會阻塞!

擁有monitor特權的線程執行完成後釋放monitor,並將計數器設置爲0;同時執行monitorexit指令;不要擔憂出現異常沒法執行monitorexit指令;爲了保證在方法異常完成時 monitorenter 和 monitorexit 指令依然能夠正確配對執行,編譯器會自動產生一個異常處理器,這個異常處理器聲明可處理全部的異常,它的目的就是用來執行 monitorexit 指令。從字節碼中也能夠看出多了一個monitorexit指令,它就是異常結束時被執行的釋放monitor 的指令。

同步代碼塊的原理了解了,那麼同步方法如何解釋?不急,咱們不妨來反編譯一下同步方法的狀態!

javap -verbose -p SynText > 3.txt

代碼

package com.byit.test;

/**
* @author huangfu
*/
public class SynText {
   public int i ;

   public synchronized void add(){
       i++;

  }
}

字節碼

Classfile /D:/2020project/byit-myth-job/demo-client/byit-demo-client/target/classes/com/byit/test/SynText.class
Last modified 2020-1-6; size 382 bytes
MD5 checksum e06926a20f28772b8377a940b0a4984f
Compiled from "SynText.java"
public class com.byit.test.SynText
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
  #1 = Methodref         #4.#17         // java/lang/Object."<init>":()V
  #2 = Fieldref           #3.#18         // com/byit/test/SynText.i:I
  #3 = Class             #19           // com/byit/test/SynText
  #4 = Class             #20           // java/lang/Object
  #5 = Utf8               i
  #6 = Utf8               I
  #7 = Utf8               <init>
  #8 = Utf8               ()V
  #9 = Utf8               Code
#10 = Utf8               LineNumberTable
#11 = Utf8               LocalVariableTable
#12 = Utf8               this
#13 = Utf8               Lcom/byit/test/SynText;
#14 = Utf8               syncTask
#15 = Utf8               SourceFile
#16 = Utf8               SynText.java
#17 = NameAndType       #7:#8         // "<init>":()V
#18 = NameAndType       #5:#6         // i:I
#19 = Utf8               com/byit/test/SynText
#20 = Utf8               java/lang/Object
{
public int i;
  descriptor: I
  flags: ACC_PUBLIC

public com.byit.test.SynText();
  descriptor: ()V
  flags: ACC_PUBLIC
  Code:
    stack=1, locals=1, args_size=1
        0: aload_0
        1: invokespecial #1                 // Method java/lang/Object."<init>":()V
        4: return
    LineNumberTable:
      line 6: 0
    LocalVariableTable:
      Start Length Slot Name   Signature
          0       5     0 this   Lcom/byit/test/SynText;

public synchronized void syncTask();
  descriptor: ()V
  flags: ACC_PUBLIC, ACC_SYNCHRONIZED
  Code:
    stack=3, locals=1, args_size=1
        0: aload_0
        1: dup
        2: getfield     #2                 // Field i:I
        5: iconst_1
        6: iadd
        7: putfield     #2                 // Field i:I
      10: return
    LineNumberTable:
      line 10: 0
      line 11: 10
    LocalVariableTable:
      Start Length Slot Name   Signature
          0     11     0 this   Lcom/byit/test/SynText;
}
SourceFile: "SynText.java"

簡化,在簡化

 public synchronized void syncTask();
  descriptor: ()V
  flags: ACC_PUBLIC, ACC_SYNCHRONIZED
  Code:
    stack=3, locals=1, args_size=1
        0: aload_0
        1: dup

咱們可以看到 flags: ACC_PUBLIC, ACC_SYNCHRONIZED這樣的一句話

從字節碼中能夠看出,synchronized修飾的方法並無monitorenter指令和monitorexit指令,取得代之的確實是ACC_SYNCHRONIZED標識,該標識指明瞭該方法是一個同步方法,JVM經過該ACC_SYNCHRONIZED訪問標誌來辨別一個方法是否聲明爲同步方法,從而執行相應的同步調用。這即是synchronized鎖在同步代碼塊和同步方法上實現的基本原理。

那麼在JAVA6以前,爲何synchronized會如此的慢?

那是由於,操做系統實現線程之間的切換須要系統內核從用戶態切換到核心態!這個狀態之間的轉換,須要較長的時間,時間成本高!因此這也就是synchronized慢的緣由!

3、鎖膨脹的過程

在這以前,你須要知道什麼是鎖膨脹!他是JAVA6以後新增的一個概念!是一種針對以前重量級鎖的一種性能的優化!他的優化,大部分是基於經驗上的一些感官,對鎖來進行優化!

①偏向鎖

研究發現,大多數狀況下,鎖不只不存在多線程競爭,並且還老是由一條線程得到!由於爲了減小鎖申請的次數!引進了偏向鎖!在沒有鎖競爭的狀況下,若是一個線程獲取到了鎖,那麼鎖就進入偏向鎖的模式!當線程再一次請求鎖時,無需申請,直接獲取鎖,進入方法!可是前提是沒有鎖競爭的狀況,存在鎖競爭,鎖會當即膨脹,膨脹爲輕量級鎖!

②輕量級鎖

偏向鎖失敗,那麼鎖膨脹爲輕量級鎖!此時鎖機構變爲輕量級鎖結構!他的經驗依據是:「絕大多數狀況下,在整個同步週期內,不會存在鎖的競爭」,故而,輕量級鎖適合,線程交替進行的場景!若是在同一時間出現兩條線程對同一把鎖的競爭,那麼此時輕量級鎖就不會生效了!可是,jdk官方爲了是鎖的優化性能更好,輕量級鎖失效後,並不會當即膨脹爲重量級鎖!而是將鎖轉換爲自旋鎖狀態!

③自旋鎖

輕量級鎖失敗後,爲了是避免線程掛起,引發內核態的切換!爲了優化,此時線程會進入自選狀態!他可能會進行幾十次,上百次的空輪訓!爲何呢?又是經驗之談!他們認爲,大多數狀況下,線程持有鎖的時間都不會太長!作幾回空輪訓,就能大機率的等待到鎖!事實證實,這種優化方式確實有效!最後若是實在等不到鎖!沒辦法,纔會完全升級爲重量級鎖!

④鎖消除

jvm在進行代碼編譯時,會基於上下文掃描;將一些不可能存在資源競爭的的鎖給消除掉!這也是JVM對於鎖的一種優化方式!不得不感嘆,jdk官方的腦子!舉個例子!在方法體類的局部變量對象,他永遠也不可能會發生鎖競爭,例如:

/**
* @author huangfu
*/
public class SynText {
   public static void add(String name1 ,String name2){
       StringBuffer sb = new StringBuffer();
       sb.append(name1).append(name2);
  }

   public static void main(String[] args) {
       for (int i = 0; i < 10000000; i++) {
           add("w"+i,"q"+i);
      }
  }
}

不可否認,StringBuffer是線程安全的!可是他永遠也不會被其餘線程引用!故而,鎖失效!故而,被消除掉!


歡迎關注做者【JAVA程序狗】



本文分享自微信公衆號 - JAVA程序狗(javacxg)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索