由淺深入理解java多線程,java并發(fā),synchronized實現(xiàn)原理及線程鎖機(jī)制

由淺深入理解java多線程,java并發(fā),synchronized實現(xiàn)原理及線程鎖機(jī)制

[TOC]

多進(jìn)程是指操作系統(tǒng)能同時運行多個任務(wù)(程序)。

多線程是指在同一程序中有多個順序流在執(zhí)行。

一,線程的生命周期

[圖片上傳失敗...(image-370bc0-1635087555179)]

  • 新建狀態(tài):

    使用 new 關(guān)鍵字和 Thread 類或其子類建立一個線程對象后,該線程對象就處于新建狀態(tài)。它保持這個狀態(tài)直到程序 start() 這個線程。

  • 就緒狀態(tài):

    當(dāng)線程對象調(diào)用了start()方法之后,該線程就進(jìn)入就緒狀態(tài)。就緒狀態(tài)的線程處于就緒隊列中,要等待JVM里線程調(diào)度器的調(diào)度。

  • 運行狀態(tài):

    如果就緒狀態(tài)的線程獲取 CPU 資源,就可以執(zhí)行 run(),此時線程便處于運行狀態(tài)。處于運行狀態(tài)的線程最為復(fù)雜,它可以變?yōu)樽枞麪顟B(tài)、就緒狀態(tài)和死亡狀態(tài)。

  • 阻塞狀態(tài):

    如果一個線程執(zhí)行了sleep(睡眠)、suspend(掛起)等方法,失去所占用資源之后,該線程就從運行狀態(tài)進(jìn)入阻塞狀態(tài)。在睡眠時間已到或獲得設(shè)備資源后可以重新進(jìn)入就緒狀態(tài)。可以分為三種:

    • 等待阻塞:運行狀態(tài)中的線程執(zhí)行 wait() 方法,使線程進(jìn)入到等待阻塞狀態(tài)。
    • 同步阻塞:線程在獲取 synchronized 同步鎖失敗(因為同步鎖被其他線程占用)。
    • 其他阻塞:通過調(diào)用線程的 sleep() 或 join() 發(fā)出了 I/O 請求時,線程就會進(jìn)入到阻塞狀態(tài)。當(dāng)sleep() 狀態(tài)超時,join() 等待線程終止或超時,或者 I/O 處理完畢,線程重新轉(zhuǎn)入就緒狀態(tài)。
  • 死亡狀態(tài):

    一個運行狀態(tài)的線程完成任務(wù)或者其他終止條件發(fā)生時,該線程就切換到終止?fàn)顟B(tài)。

二,線程的調(diào)度

調(diào)整線程優(yōu)先級

Java線程有優(yōu)先級,優(yōu)先級高的線程會獲得較多的運行機(jī)會。

線程睡眠

Thread.sleep(long millis)方法,使線程轉(zhuǎn)到阻塞狀態(tài)。millis參數(shù)設(shè)定睡眠的時間,以毫秒為單 位。當(dāng)睡眠結(jié)束后,就轉(zhuǎn)為就緒(Runnable)狀態(tài)。sleep()平臺移植性好。

線程等待

Object類中的wait()方法,導(dǎo)致當(dāng)前的線程等待,直到其他線程調(diào)用此對象的 notify() 方法或 notifyAll() 喚醒方法。這個兩個喚醒方法也是Object類中的方法,行為等價于調(diào)用 wait(0) 一樣。 notifyAll() 喚醒方法。這個兩個喚醒方法也是Object類中的方法,行為等價于調(diào)用 wait(0) 一樣。

線程讓步

Thread.yield() 方法,暫停當(dāng)前正在執(zhí)行的線程對象,把執(zhí)行機(jī)會讓給相同或者更高優(yōu)先級的線 程。

線程加入

join()方法,等待其他線程終止。在當(dāng)前線程中調(diào)用另一個線程的join()方法,則當(dāng)前線程轉(zhuǎn)入阻 塞狀態(tài),直到另一個進(jìn)程運行結(jié)束,當(dāng)前線程再由阻塞轉(zhuǎn)為就緒狀態(tài)。

線程喚醒

Object類中的notify()方法,喚醒在此對象監(jiān)視器上等待的單個線程。如果所有線程都在此對象上等待,則會選擇喚醒其中一個線程。選擇是任意性的,并在對實現(xiàn)做出決定時發(fā)生。線程通過調(diào)用其中一個 wait 方法,在對象的監(jiān)視器上等待。 直到當(dāng)前的線程放棄此對象上的鎖定,才能繼續(xù)執(zhí)行被喚醒的線程。被喚醒的線程將以常規(guī)方式與在該對象上主動同步的其他所有線程進(jìn)行競爭;例如,喚醒的線程在作為鎖定此對象 的下一個線程方面沒有可靠的特權(quán)或劣勢。類似的方法還有一個notifyAll(),喚醒在此對象監(jiān)視器上等待的所有線程。

三,創(chuàng)建多線程的方式

1,通過實現(xiàn)Runnable接口

//
public class T3 implements Runnable {
    String a;
    //構(gòu)造方法
    public T3(String a) {
        this.a = a;
    }

    public void run() {
        System.out.println(a);
    }
}
//開啟了兩個線程,實例化了兩個對象,但是現(xiàn)在還沒有做數(shù)據(jù)共享的驗證
public static void main(String[] args) {
    
    new Thread(new T3("上海")).start();
    new Thread(new T3("北京")).start();

}

使用接口,在啟動的多線程的時候,需要先通過 Thread 類的構(gòu)造方法 Thread(Runnable target) 構(gòu)造出對象,然后調(diào)用 Thread 對象的 start() 方法來運行多線程代碼。

輸出結(jié)果:

結(jié)果1 結(jié)果2

北京 上海

上海 北京

2,通過繼承Thread類

//
public class T1 extends Thread {
    String a;
    //構(gòu)造方法
    public T1(String a) {
        this.a = a;
    }
    
    public void run() {
        System.out.println(a);
    }
}
//開啟了兩個線程,實例化了兩個對象,但是現(xiàn)在還沒有做數(shù)據(jù)共享的驗證
public class T2 { 
    public static void main(String[] args) {

        new T1("上海").start();

        new T1("北京").start();
    }
}

輸出結(jié)果:

結(jié)果1 結(jié)果2

北京 上海

上海 北京

四,多線程間的數(shù)據(jù)共享

1,Runnable接口實現(xiàn)多線程的數(shù)據(jù)共享

//寫法1
public class T3 implements Runnable {
    
  int b = 10;
  String a;
  
    public T3(String a) {
        this.a = a;
    }
    
  public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(a + b--);
        }
    }  
}

//寫法2
public class T3 implements Runnable {
    int b = 10;
    
  public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + b--);
        }
    }
    
}
//開啟了兩個線程,實例化了1個對象
public class T4 {
    
  public static void main(String[] args) {
    T3 t3 = new T3();
        new Thread(t3, "上海").start();
        new Thread(t3, "北京").start();
    }

}

兩個線程,一起操作同一個數(shù)值,每個線程各操作5次,未出現(xiàn)重復(fù)的數(shù)值,實現(xiàn)數(shù)據(jù)共享。部分輸出結(jié)果為:

輸出結(jié)果1 輸出結(jié)果2 輸出結(jié)果3 輸出結(jié)果4 輸出結(jié)果5
上海10 上海10 上海10 上海10 北京9
上海9 上海9 北京9 北京9 北京8
上海8 上海8 北京7 北京7 北京7
上海6 上海7 北京6 北京6 北京6
上海5 上海6 北京5 上海8 北京5
北京7 北京5 北京4 上海4 上海10
北京4 北京4 上海8 上海3 上海4
北京3 北京3 上海3 北京5 上海3
北京2 北京2 上海2 上海2 上海2
北京1 北京1 上海1 北京1 上海1

2,Thread類實現(xiàn)多線程的數(shù)據(jù)共享

不方便做到

//
public class T1 extends Thread {
    int b = 10;

    String a;
    public T1(String a) {
        this.a = a;
    }

    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(a + b--);
        }
    }

}
//開啟了兩個線程,實例化了2個對象
public class T2 { 
    public static void main(String[] args) {

        new T1("上海").start();

        new T1("北京").start();
    }
}

輸出結(jié)果

兩個線程,一起操作同一個數(shù)值,每個線程各操作5次,出現(xiàn)了重復(fù)的數(shù)值,未實現(xiàn)數(shù)據(jù)共享。部分輸出結(jié)果為:

輸出結(jié)果1 輸出結(jié)果2 輸出結(jié)果3 輸出結(jié)果4 輸出結(jié)果5
上海10 北京10 上海10 上海10 上海10
上海9 北京9 北京10 上海9 北京10
上海8 北京8 上海9 上海8 北京9
上海7 北京7 上海8 北京10 上海9
上海6 北京6 北京9 北京9 北京8
北京10 上海10 北京8 北京8 上海8
北京9 上海9 北京7 北京7 北京7
北京8 上海8 北京6 北京6 上海7
北京7 上海7 上海7 上海7 北京6
北京6 上海6 上海6 上海6 上海6

總結(jié)

實現(xiàn) Runnable 接口比繼承 Thread 類所具有的優(yōu)勢:

1):適合多個相同的程序代碼的線程去處理同一個資源

2):可以避免 java 中的單繼承的限制

五,synchronized實現(xiàn)多線程數(shù)據(jù)共享

當(dāng)存在多個線程操作共享數(shù)據(jù)時,需要保證同一時刻有且只有一個線程在操作共享數(shù)據(jù),其他線程必須等到該線程處理完數(shù)據(jù)后再進(jìn)行。

當(dāng)兩個并發(fā)線程訪問同一個對象中的 synchronized 代碼塊時,在同一時刻只能有一個線程得到執(zhí)行,另一個線程受阻塞,必須等待當(dāng)前線程執(zhí)行完這個代碼塊以后才能執(zhí)行該代碼塊。此時線程是互斥的,因為在執(zhí)行代碼塊時會鎖定當(dāng)前的對象,只有執(zhí)行完該代碼塊才能釋放該對象鎖,下一個線程才能執(zhí)行并鎖定該對象。

1,修飾實例方法

通過Runnable接口

//
public class T3 implements Runnable {
    int b = 10;

    public synchronized int aaa() {
        return b--;
    }

    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + aaa());
        }
    }

}
//開啟了兩個線程,實例化了1個對象
public class T4 {

    public static void main(String[] args) {
        T3 t3 = new T3();
        new Thread(t3, "上海").start();
        new Thread(t3, "北京").start();
    }

}

輸出結(jié)果;參照線程間數(shù)據(jù)共享的Runnable接口的輸出結(jié)果。可實現(xiàn)數(shù)據(jù)共享

Thread類

public class T1 extends Thread {
    int b = 10;

    String a;

    public T1(String a) {
        this.a = a;
    }

    public synchronized int aaa() {
        return b--;
    }

    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(a + aaa());
        }
    }

}
//開啟了兩個線程,實例化了2個對象
public class T2 {
    public static void main(String[] args) {

        new T1("上海").start();

        new T1("北京").start();
    }
}

輸出結(jié)果;參照線程間數(shù)據(jù)共享的Thread類的輸出結(jié)果。沒有實現(xiàn)數(shù)據(jù)共享

如果是一個線程 A 需要訪問實例對象 obj1 的 synchronized 方法 f1(當(dāng)前對象鎖是obj1),另一個線程 B 需要訪問實例對象 obj2 的 synchronized 方法 f2(當(dāng)前對象鎖是obj2),這樣是允許的,因為兩個實例對象鎖并不同相同, 此時如果兩個線程操作數(shù)據(jù)并非共享的。

雖然我們使用synchronized修飾了 aaa 方法,但卻new了兩個不同的實例對象,這也就意味著存在著兩個不同的實例對象鎖,因此t1和t2都會進(jìn)入各自的對象鎖,也就是說t1和t2線程使用的是不同的鎖,因此線程安全是無法保證的。

2,修飾靜態(tài)方法

通過Runnable接口

//
public class T3 implements Runnable {
    static int b = 10;

    public static synchronized int aaa() {
        return b--;
    }

    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + aaa());
        }
    }

}
//開啟了兩個線程,實例化了1個對象
public class T4 {

    public static void main(String[] args) {
        T3 t3 = new T3();
        new Thread(t3, "上海").start();
        new Thread(t3, "北京").start();
    }

}

輸出結(jié)果;參照線程間數(shù)據(jù)共享的Runnable接口的輸出結(jié)果。可實現(xiàn)數(shù)據(jù)共享

Thread類

public class T1 extends Thread {
    static int b = 10;

    String a;

    public T1(String a) {
        this.a = a;
    }

    public static synchronized int aaa() {
        return b--;
    }

    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(a + aaa());
        }
    }

}
//開啟了兩個線程,實例化了2個對象
public class T2 {
    public static void main(String[] args) {

        new T1("上海").start();

        new T1("北京").start();
    }
}

輸出結(jié)果;參照線程間數(shù)據(jù)共享的Runnable接口的輸出結(jié)果。可實現(xiàn)數(shù)據(jù)共享

synchronized作用于靜態(tài)的 aaa 方法,這樣的話,對象鎖就當(dāng)前類對象,由于無論創(chuàng)建多少個實例對象,但對于的類對象擁有只有一個,所有在這樣的情況下對象鎖就是唯一的。

3,修飾同步代碼塊

能縮小代碼段的范圍就盡量縮小,能在代碼段上加同步就不要再整個方法上加同步。這叫減小鎖的粒度,使代碼更大程度的并發(fā)。鎖的代碼段太長了,別的線程就要等很久,等的花兒都謝了。

通過Runnable接口

//
public class T3 implements Runnable {
    int b = 10;

    public void run() 
        synchronized (this) {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + aaa());
            }
        }
    }

}
public class T4 {

    public static void main(String[] args) {
        T3 t3 = new T3();
        new Thread(t3, "上海").start();
        new Thread(t3, "北京").start();
    }

}

4,總結(jié)

  • start()方法的調(diào)用后并不是立即執(zhí)行多線程代碼,而是使得該線程變?yōu)榭蛇\行態(tài)(Runnable),什么時候運行是由操作系統(tǒng)決定的。

  • 請記住,上下文的切換開銷也很重要,如果你創(chuàng)建了太多的線程,CPU 花費在上下文的切換的時間將多于執(zhí)行程序的時間!

  • 沒有 synchronized 關(guān)鍵字的默認(rèn)情況。如線程間數(shù)據(jù)共享一節(jié)中并沒有用該關(guān)鍵字。
  • 實例化多個對象,也就存在多個對象鎖,每個線程用不同的對象鎖,數(shù)據(jù)自然無法共享。
  • 不管實例化多少個對象,如果synchronized作用于靜態(tài)方法,由于靜態(tài)的特殊性,該對象只會有一個,那么在這樣的情況下對象鎖又是唯一的。

六,synchronized實現(xiàn)原理

1,synchronized修飾后的字節(jié)碼

上述synchronized主要是了解數(shù)據(jù)共享的,其字節(jié)碼并不直觀看鎖相關(guān)的,另外寫了個如下所示;

public class T5 {

    //修飾方法
    public synchronized void aaa(){
        
    }

    //修飾靜態(tài)方法
    public static synchronized void bbb(){
        
    }

    //修飾類
    public void ccc(){
        synchronized (T5.class){
            
        }
    }

    //修飾this
    public void ddd(){
        synchronized (this){
            
        }
    }

}

window下取其字節(jié)碼內(nèi)容

image-20211020195414783

javac T5.java 編譯生成class文件

javap -v -p -s -sysinfo -constants T5.class ,使用javap 工具查看生成的class文件

Classfile /D:/Test/Java/src/com/lgx/test/T5.class
  Last modified 2021-10-20; size 549 bytes
  MD5 checksum f3500e41224be759d110519587593b09
  Compiled from "T5.java"
public class com.lgx.test.T5
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #3.#18         // java/lang/Object."<init>":()V
   #2 = Class              #19            // com/lgx/test/T5
   #3 = Class              #20            // java/lang/Object
   #4 = Utf8               <init>
   #5 = Utf8               ()V
   #6 = Utf8               Code
   #7 = Utf8               LineNumberTable
   #8 = Utf8               aaa
   #9 = Utf8               bbb
  #10 = Utf8               ccc
  #11 = Utf8               StackMapTable
  #12 = Class              #19            // com/lgx/test/T5
  #13 = Class              #20            // java/lang/Object
  #14 = Class              #21            // java/lang/Throwable
  #15 = Utf8               ddd
  #16 = Utf8               SourceFile
  #17 = Utf8               T5.java
  #18 = NameAndType        #4:#5          // "<init>":()V
  #19 = Utf8               com/lgx/test/T5
  #20 = Utf8               java/lang/Object
  #21 = Utf8               java/lang/Throwable
{
  public com.lgx.test.T5();
    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 3: 0

  public synchronized void aaa();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 6: 0

  public static synchronized void bbb();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
    Code:
      stack=0, locals=0, args_size=0
         0: return
      LineNumberTable:
        line 9: 0

  public void ccc();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: ldc           #2                  // class com/lgx/test/T5
         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: return
      Exception table:
         from    to  target type
             5     7    10   any
            10    13    10   any
      LineNumberTable:
        line 12: 0
        line 13: 5
        line 14: 15
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 10
          locals = [ class com/lgx/test/T5, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4

  public void ddd();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter
         4: aload_1
         5: monitorexit
         6: goto          14
         9: astore_2
        10: aload_1
        11: monitorexit
        12: aload_2
        13: athrow
        14: return
      Exception table:
         from    to  target type
             4     6     9   any
             9    12     9   any
      LineNumberTable:
        line 17: 0
        line 18: 4
        line 19: 14
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 9
          locals = [ class com/lgx/test/T5, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4
}
SourceFile: "T5.java"

由字節(jié)碼可知,當(dāng)修飾方法時,JVM采用 ACC_SYNCHRONIZED 標(biāo)記符來實現(xiàn)同步。 當(dāng)修飾類時,JVM采用monitorenter、monitorexit兩個指令來實現(xiàn)同步。(在字節(jié)碼里面可以看見在修飾類時,有Exception table,這是因為,JVM會自動在synchronized代碼塊中加入異常捕獲,從而保證代碼拋出異常時,仍能夠釋放當(dāng)前線程占用的鎖,避免出現(xiàn)死鎖現(xiàn)象。)

在synchronized修飾方法時是添加ACC_SYNCHRONIZED標(biāo)識。方法級同步是隱式執(zhí)行的,作為方法調(diào)用和返回的一部分。 同步方法在運行時常量池的 method_info 結(jié)構(gòu)中通過 ACC_SYNCHRONIZED 標(biāo)志進(jìn)行區(qū)分,該標(biāo)志由方法調(diào)用指令檢查。 當(dāng)調(diào)用設(shè)置了 ACC_SYNCHRONIZED 的方法時,執(zhí)行線程進(jìn)入監(jiān)視器(monitor),調(diào)用方法本身,并退出monitor,無論方法調(diào)用是正常完成還是突然完成。 在執(zhí)行線程擁有monitor期間,沒有其他線程可以進(jìn)入它。 如果在調(diào)用同步方法過程中拋出異常并且同步方法沒有處理該異常,則在異常重新拋出同步方法之前,該方法的monitor會自動退出。

在synchronized修飾類時是通過monitorenter、monitorexit指令。 當(dāng)且僅當(dāng)monitor有所有者時,monitor才被鎖定。 執(zhí)行monitorenter 的線程嘗試獲得與objectref 關(guān)聯(lián)的monitor的所有權(quán),如下所示:

  • 如果與objectref 關(guān)聯(lián)的monitor的條目計數(shù)為零,則該線程進(jìn)入monitor并將其條目計數(shù)設(shè)置為1,然后該線程是monitor的所有者。
  • 如果線程已經(jīng)擁有與 objectref 關(guān)聯(lián)的monitor,它會重新進(jìn)入monitor,增加其條目計數(shù)。
  • 如果另一個線程已經(jīng)擁有與 objectref 關(guān)聯(lián)的monitor,線程會阻塞,直到monitor的條目計數(shù)為零,然后再次嘗試獲得所有權(quán)

同理,執(zhí)行monitorexit 的線程必須是與objectref 引用的實例關(guān)聯(lián)的monitor的所有者。該線程遞減與objectref 關(guān)聯(lián)的monitor的入口計數(shù),如果結(jié)果條目計數(shù)的值為零,則線程退出monitor并且不再是其所有者。

在了解monitor之前,還需先大概了解對象頭這個概念。

2,對象頭

在hotspot虛擬機(jī)中,對象在內(nèi)存的分布分為3個部分:對象頭,實例數(shù)據(jù),和對齊填充。

image-20211021134521978
  • 實例變量:存放類的屬性數(shù)據(jù)信息。 包括父類的屬性信息,如果是數(shù)組的實例部分還包括數(shù)組的長度,這部分內(nèi)存按4字節(jié)對齊。

  • 填充數(shù)據(jù):用于保證對象8字節(jié)對齊。 由于虛擬機(jī)要求對象起始地址必須是8字節(jié)的整數(shù)倍。填充數(shù)據(jù)不是必須存在的,僅僅是為了字節(jié)對齊。

  • 對象頭:jvm采用2個字寬(Word)存儲對象頭,若對象為數(shù)組則采用3個字寬來存儲。在32位虛擬機(jī)中1字寬等于4字節(jié),64位虛擬機(jī)中1字寬等于8字節(jié)。synchronized使用的鎖對象是存儲在Java對象頭里的,jvm中采用2個字來存儲對象頭,如果對象是數(shù)組則會分配3個字,多出來的1個字記錄的是數(shù)組長度,其結(jié)構(gòu)說明如下表:

長度 頭對象結(jié)構(gòu) 說明
32/64bit Mark Word 存儲對象的hashCode、鎖信息或分代年齡或GC標(biāo)志等信息
32/64bit Class Metadata Address 類型指針指向?qū)ο蟮念愒獢?shù)據(jù),JVM通過這個指針確定該對象是哪個類的實例。
32/32bit Array length 數(shù)組的長度(若當(dāng)前對象為數(shù)組)

由于對象頭的信息是與對象自身定義的數(shù)據(jù)沒有關(guān)系的額外存儲成本,因此考慮到JVM的空間效率,Mark Word 被設(shè)計成為一個非固定的數(shù)據(jù)結(jié)構(gòu),以便存儲更多有效的數(shù)據(jù)。64位JVM下,如下所示;

鎖狀態(tài) 25bit 4bit 1bit是否是偏向鎖 2bit 鎖標(biāo)志位
無鎖狀態(tài) 對象HashCode 對象分代年齡 0 01

它會根據(jù)對象本身的狀態(tài)復(fù)用自己的存儲空間,如32位JVM下,除了上述列出的Mark Word默認(rèn)存儲結(jié)構(gòu)外,還有如下可能變化的結(jié)構(gòu):

Mark Word????????????

monitor對象存在于每個Java對象的對象頭中,synchronized鎖便是通過這種方式獲取鎖的,也是為什么Java中任意對象可以作為鎖的原因。

3,monitor

指向互斥量的指針指向的就是monitor對象的起始地址。在Java虛擬機(jī)(HotSpot)中,monitor是由ObjectMonitor實現(xiàn)的,其主要數(shù)據(jù)結(jié)構(gòu)如下

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; //記錄個數(shù)
    _waiters      = 0, //等待線程數(shù)
    _recursions   = 0; //重入次數(shù)
    _object       = NULL;//存儲該monitor的對象
    _owner        = NULL;//指向獲得monitor的ObjectWaiter對象
    _WaitSet      = NULL; //處于wait狀態(tài)的線程,會被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;//多線程競爭鎖時的單向列表
    FreeNext      = NULL ;
    _EntryList    = NULL ; //處于等待鎖block狀態(tài)的線程,會被加入到該列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
}

ObjectMonitor中有兩個隊列,WaitSet 和 _EntryList,用來保存ObjectWaiter對象列表( 每個等待鎖的線程都會被封裝成ObjectWaiter對象),owner指向持有ObjectMonitor對象的線程,當(dāng)多個線程同時訪問一段同步代碼時,首先會進(jìn)入 _EntryList 集合,當(dāng)線程獲取到對象的monitor 后進(jìn)入 _Owner 區(qū)域并把monitor中的owner變量設(shè)置為當(dāng)前線程同時monitor中的計數(shù)器count加1,若線程調(diào)用 wait() 方法,將釋放當(dāng)前持有的monitor,owner變量恢復(fù)為null,count自減1,同時該線程進(jìn)入 WaitSet集合中等待被喚醒。若當(dāng)前線程執(zhí)行完畢也將釋放monitor(鎖)并復(fù)位變量的值,以便其他線程進(jìn)入獲取monitor(鎖)。

如下圖所示,一個線程通過1號門進(jìn)入Entry Set(入口區(qū)),如果在入口區(qū)沒有線程等待,那么這個線程就會獲取監(jiān)視器成為監(jiān)視器的Owner,然后執(zhí)行監(jiān)視區(qū)域的代碼。如果在入口區(qū)中有其它線程在等待,那么新來的線程也會和這些線程一起等待。線程在持有監(jiān)視器的過程中,有兩個選擇,一個是正常執(zhí)行監(jiān)視器區(qū)域的代碼,釋放監(jiān)視器,通過5號門退出監(jiān)視器;還有可能等待某個條件的出現(xiàn),于是它會通過3號門到Wait Set(等待區(qū))休息,直到相應(yīng)的條件滿足后再通過4號門進(jìn)入重新獲取監(jiān)視器再執(zhí)行。

當(dāng)一個線程釋放監(jiān)視器時,在入口區(qū)和等待區(qū)的等待線程都會去競爭監(jiān)視器,如果入口區(qū)的線程贏了,會從2號門進(jìn)入;如果等待區(qū)的線程贏了會從4號門進(jìn)入。只有通過3號門才能進(jìn)入等待區(qū),在等待區(qū)中的線程只有通過4號門才能退出等待區(qū),也就是說一個線程只有在持有監(jiān)視器時才能執(zhí)行wait操作,處于等待的線程只有再次獲得監(jiān)視器才能退出等待狀態(tài)。

image-20211021143934832

monitor并不是隨著對象創(chuàng)建而創(chuàng)建的。而是每個線程都存在兩個ObjectMonitor對象列表,分別為free和used列表;同時jvm中也維護(hù)著global locklist。當(dāng)線程需要ObjectMonitor對象時,首先從自身的free表中申請,若存在則使用,若不存在則從global list中申請。

monitor是線程私有的數(shù)據(jù)結(jié)構(gòu),每一個線程都有一個可用monitor列表,同時還有一個全局的可用列表,monitor的內(nèi)部如下所示,

img
  • Owner:初始時為NULL表示當(dāng)前沒有任何線程擁有該monitor,當(dāng)線程成功擁有該鎖后保存線程唯一標(biāo)識,當(dāng)鎖被釋放時又設(shè)置為NULL;

  • EntryQ:關(guān)聯(lián)一個系統(tǒng)互斥鎖(semaphore),阻塞所有試圖鎖住monitor失敗的線程。

  • RcThis:表示blocked或waiting在該monitor上的所有線程的個數(shù)。

  • Nest:用來實現(xiàn)重入鎖的計數(shù)。

  • HashCode:保存從對象頭拷貝過來的HashCode值(可能還包含GC age)。

  • Candidate:用來避免不必要的阻塞或等待線程喚醒,因為每一次只有一個線程能夠成功擁有鎖,如果每次前一個釋放鎖的線程喚醒所有正在阻塞或等待的線程,會引起不必要的上下文切換(從阻塞到就緒然后因為競爭鎖失敗又被阻塞)從而導(dǎo)致性能嚴(yán)重下降。Candidate只有兩種可能的值:0表示沒有需要喚醒的線程,1表示要喚醒一個繼任線程來競爭鎖。

4,小結(jié)

JVM 是通過進(jìn)入、退出 對象監(jiān)視器(Monitor) 來實現(xiàn)對方法、同步塊的同步的,而對象監(jiān)視器的本質(zhì)依賴于底層操作系統(tǒng)的 互斥鎖(Mutex Lock) 實現(xiàn)。具體實現(xiàn)是在編譯之后在同步方法調(diào)用前加入一個monitor.enter指令,在退出方法和異常處插入monitor.exit的指令。對于沒有獲取到鎖的線程將會阻塞到方法入口處,直到獲取鎖的線程monitor.exit之后才能嘗試?yán)^續(xù)獲取鎖。

當(dāng)執(zhí)行monitorenter指令時,線程試圖獲取鎖也就是獲取monitor的持有權(quán)。當(dāng)計數(shù)器為0則可以成功獲取,獲取后將鎖計數(shù)器設(shè)為1也就是加1。相應(yīng)的在執(zhí)行monitorexit指令后,將鎖計數(shù)器設(shè)為0,表明鎖被釋放。如果獲取對象鎖失敗,那當(dāng)前線程就要阻塞等待,直到鎖被另外一個線程釋放為止。

img

從synchronized的特點中可以看到它是一種重量級鎖,會涉及到操作系統(tǒng)狀態(tài)的切換影響效率,所以JDK1.6中對synchronized進(jìn)行了各種優(yōu)化,為了能減少獲取和釋放鎖帶來的消耗引入了偏向鎖和輕量鎖。

七,鎖機(jī)制

隨著鎖的競爭,鎖可以從偏向鎖升級到輕量級鎖,再升級的重量級鎖,但是鎖的升級是單向的,也就是說只能從低到高升級,不會出現(xiàn)鎖的降級。

Mark Word中的數(shù)據(jù)隨著鎖標(biāo)志位的變化而變化,如下

mark

1,偏向鎖

偏向鎖是Java 6之后加入的新鎖,它是一種針對加鎖操作的優(yōu)化手段。經(jīng)過研究發(fā)現(xiàn),在大多數(shù)情況下,鎖不僅不存在多線程競爭,而且總是被同一線程多次獲得,因此為了減少這同一線程獲取鎖的代價而引入偏向鎖(看來社會上的二八法則也存在于這里)。

偏向鎖的獲取:當(dāng)一個線程訪問同步塊并獲取鎖時,會在對象頭和棧幀中的鎖記錄里存儲鎖偏向的線程ID,以后該線程在進(jìn)入和退出同步塊時不需要進(jìn)行CAS操作來加鎖和解釋,只需要簡單地測試一下對象頭的Mark Word里是否存儲著指向當(dāng)前線程的偏向鎖。如果是,則直接獲得鎖,執(zhí)行同步塊;如果不是,則使用CAS操作更改線程ID,更改成功獲得鎖,更改失敗開始撤銷偏向鎖。

偏向鎖的釋放:偏向鎖只有存在鎖競爭的情況下才會釋放。撤銷偏向鎖需要等待全局安全點(在這個時間點上沒有正在執(zhí)行的字節(jié)碼),首先暫停擁有偏向鎖的線程,然后檢查此線程是否活著,如果線程不處于活動狀態(tài),則轉(zhuǎn)成無鎖狀態(tài);如果還活著,升級為輕量級鎖。下圖線程1展示了偏向鎖獲取的過程,線程2展示了偏向鎖撤銷的過程。

mark

偏向鎖的關(guān)閉:偏向鎖在Java 6和Java 7里是默認(rèn)啟用的,但是它在應(yīng)用程序啟動幾秒鐘之后才激活,如有必要可以使用JVM參數(shù)來關(guān)閉延遲:-XX:BiasedLockingStartupDelay=0。如果你確定應(yīng)用程序里所有的鎖通常情況下處于競爭狀態(tài),可以通過JVM參數(shù)關(guān)閉偏向鎖:-XX:-UseBiasedLocking=false,那么程序默認(rèn)會進(jìn)入輕量級鎖狀態(tài)。

對于沒有鎖競爭的場合,偏向鎖有很好的優(yōu)化效果,畢竟極有可能連續(xù)多次是同一個線程申請相同的鎖。但是對于鎖競爭比較激烈的場合,偏向鎖就失效了,因為這樣場合極有可能每次申請鎖的線程都是不相同的,因此這種場合下不應(yīng)該使用偏向鎖,否則會得不償失,需要注意的是,偏向鎖失敗后,并不會立即膨脹為重量級鎖,而是先升級為輕量級鎖。

2,輕量級鎖

倘若偏向鎖失敗,虛擬機(jī)并不會立即升級為重量級鎖,它還會嘗試使用一種稱為輕量級鎖的優(yōu)化手段(1.6之后加入的),此時Mark Word 的結(jié)構(gòu)也變?yōu)檩p量級鎖的結(jié)構(gòu)。輕量級鎖并不是用來代替重量級鎖的,它的本意是在沒有多線程競爭的前提下,減少傳統(tǒng)的重量級鎖使用產(chǎn)生的性能消耗。輕量級鎖能夠提升程序性能的依據(jù)是“對絕大部分的鎖,在整個同步周期內(nèi)都不存在競爭”,注意這是經(jīng)驗數(shù)據(jù)。需要了解的是,輕量級鎖所適應(yīng)的場景是線程交替執(zhí)行同步塊的場合,如果存在同一時間訪問同一鎖的場合,就會導(dǎo)致輕量級鎖膨脹為重量級鎖。

輕量鎖的獲取:線程在執(zhí)行同步塊之前,JVM會先在當(dāng)前線程的棧楨中創(chuàng)建用于存儲鎖記錄的空間,并將對象頭中的Mark Word復(fù)制到鎖記錄中,官方稱為Displaced Mark Word。然后線程嘗試使用CAS將對象頭中的Mark Word替換為指向鎖記錄的指針。如果成功,當(dāng)前線程獲得鎖,如果失敗,表示其他線程競爭鎖,當(dāng)前線程便嘗試使用自旋來獲取鎖。

輕量鎖的釋放:輕量級解鎖時,會使用原子的CAS操作將Displaced Mark Word替換回到對象頭,如果成功,則表示沒有競爭發(fā)生。如果失敗,表示當(dāng)前鎖存在競爭,鎖就會膨脹成重量級鎖。下圖是兩個線程同時爭奪鎖,導(dǎo)致鎖膨脹的流程圖。

mark

3,重量級鎖

因為自旋會消耗CPU,為了避免無用的自旋(比如獲得鎖的線程被阻塞住了),一旦鎖升級成重量級鎖,就不會再恢復(fù)到輕量級鎖狀態(tài)。當(dāng)鎖處于這個狀態(tài)下,其他線程試圖獲取鎖時,都會被阻塞住,當(dāng)持有鎖的線程釋放鎖之后會喚醒這些線程,被喚醒的線程就會進(jìn)行新一輪的奪鎖之爭。

4,小結(jié)

優(yōu)點 缺點 適用場景
偏向鎖 加鎖和解鎖不需要額外的消耗,和執(zhí)行非同步方法相比僅存在納秒級的差距 如果線程間存在鎖競爭,會帶來額外的鎖撤銷的消耗 適用于只有一個線程訪問同步塊場景(只有一個線程進(jìn)入臨界區(qū))
輕量級鎖 競爭的線程不會阻塞,提高了程序的響應(yīng)速度 如果始終得不到索競爭的線程,使用自旋會消耗CPU 追求響應(yīng)速度,同步塊執(zhí)行速度非常快(多個線程交替進(jìn)入臨界區(qū))
重量級鎖 線程競爭不使用自旋,不會消耗CPU 線程阻塞,響應(yīng)時間緩慢 追求吞吐量,同步塊執(zhí)行速度較慢(多個線程同時進(jìn)入臨界區(qū))

八,拓展

1,CAS操作

使用鎖時,線程獲取鎖是一種悲觀鎖策略,即假設(shè)每一次執(zhí)行臨界區(qū)代碼都會產(chǎn)生沖突,所以當(dāng)前線程獲取到鎖的時候同時也會阻塞其他線程獲取該鎖。而CAS操作(又稱為無鎖操作)是一種樂觀鎖策略,它假設(shè)所有線程訪問共享資源的時候不會出現(xiàn)沖突,既然不會出現(xiàn)沖突自然而然就不會阻塞其他線程的操作。因此,線程就不會出現(xiàn)阻塞停頓的狀態(tài)。那么,如果出現(xiàn)沖突了怎么辦?無鎖操作是使用CAS(compare and swap)又叫做比較交換來鑒別線程是否出現(xiàn)沖突,出現(xiàn)沖突就重試當(dāng)前操作直到?jīng)]有沖突為止。

CAS包含三個值:V 內(nèi)存地址存放的實際值;O 預(yù)期的值(舊值);N 更新的新值。當(dāng)V和O相同時,也就是說舊值和內(nèi)存中實際的值相同表明該值沒有被其他線程更改過,即該舊值O就是目前來說最新的值了,自然而然可以將新值N賦值給V。反之,V和O不相同,表明該值已經(jīng)被其他線程改過了則該舊值O不是最新版本的值了,所以不能將新值N賦給V,返回V即可。當(dāng)多個線程使用CAS操作一個變量是,只有一個線程會成功,并成功更新,其余會失敗。失敗的線程會重新嘗試,當(dāng)然也可以選擇掛起線程。

簡單來說,就是CPU去更新一個值,但如果想改的值不再是原來的值,操作就失敗,因為很明顯,有其它操作先改變了這個值。就是指當(dāng)兩者進(jìn)行比較時,如果相等,則證明共享數(shù)據(jù)沒有被修改,替換成新值,然后繼續(xù)往下運行;如果不相等,說明共享數(shù)據(jù)已經(jīng)被修改,放棄已經(jīng)所做的操作,然后重新執(zhí)行剛才的操作。容易看出 CAS 操作是基于共享數(shù)據(jù)不會被修改的假設(shè),采用了類似于數(shù)據(jù)庫的commit-retry 的模式。當(dāng)同步?jīng)_突出現(xiàn)的機(jī)會很少時,這種假設(shè)能帶來較大的性能提升。

2,CAS問題

1,ABA問題

因為CAS會檢查舊值有沒有變化,這里存在這樣一個有意思的問題。比如一個舊值A(chǔ)變?yōu)榱顺葿,然后再變成A,剛好在做CAS時檢查發(fā)現(xiàn)舊值并沒有變化依然為A,但是實際上的確發(fā)生了變化。解決方案可以沿襲數(shù)據(jù)庫中常用的樂觀鎖方式,添加一個版本號可以解決。原來的變化路徑A->B->A就變成了1A->2B->3C。

2,自旋時間過長

使用CAS時非阻塞同步,也就是說不會將線程掛起,會自旋(簡單來說就是一直循環(huán))進(jìn)行下一次嘗試,如果這里自旋時間過長對性能是很大的消耗。

3,只能保證一個共享變量的原子操作

當(dāng)對一個共享變量執(zhí)行操作時CAS能保證其原子性,如果對多個共享變量進(jìn)行操作,CAS就不能保證其原子性。但可以通過新建一個類,其中的成員變量就是這幾個共享變量,然后將這個對象做CAS操作就可以保證其原子性(atomic中提供了AtomicReference來保證引用對象之間的原子性)

3,樂觀鎖

樂觀鎖是一種樂觀思想,即認(rèn)為讀多寫少,遇到并發(fā)寫的可能性低,每次去拿數(shù)據(jù)的時候都認(rèn)為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數(shù)據(jù),采取在寫時先讀出當(dāng)前版本號,然后加鎖操作(比較跟上一次的版本號,如果一樣則更新),如果失敗則要重復(fù)讀-比較-寫的操作。java中的樂觀鎖基本都是通過CAS操作實現(xiàn)的,CAS是一種更新的原子操作,比較當(dāng)前值跟傳入值是否一樣,一樣則更新,否則失敗。

4,悲觀鎖

悲觀鎖是就是悲觀思想,即認(rèn)為寫多,遇到并發(fā)寫的可能性高,每次去拿數(shù)據(jù)的時候都認(rèn)為別人會修改,所以每次在讀寫數(shù)據(jù)的時候都會上鎖,這樣別人想讀寫這個數(shù)據(jù)就會block直到拿到鎖。java中的悲觀鎖就是Synchronized,AQS框架下的鎖則是先嘗試cas樂觀鎖去獲取鎖,獲取不到,才會轉(zhuǎn)換為悲觀鎖,如RetreenLock。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容