java多線程4:synchronized關鍵字

java多線程4:synchronized關鍵字

概述
java有各種各樣的鎖,並且每種鎖的特性不同,合理場景下利用鎖可以展現出非常高的效率。synchronized內置鎖就是Java的一種重量級鎖,它能夠解決併發編程中出現多個線程同時訪問一個共享,可變的臨界資源時出現的線程安全問題。讓多個線程序列化訪問臨界資源,同一時刻,只能有一個線程訪問臨界資源,同步互斥,這樣就保證了操作的原子性。

synchronized使用
同步方法塊

public class ThreadDemo5 implements Runnable{

private int count = 0;

@Override
public void run() {
    synchronized (this){
        for (int i = 0; i < 10; ++i){
            count++;
            System.out.println("執行的線程是=>" + Thread.currentThread().getName() + "執行結果爲->" + count);
        }
    }

}

public static void main(String[] args) {
    ThreadDemo5 threadDemo5 = new ThreadDemo5();
    Thread thread1 = new Thread(threadDemo5,"thread1");
    Thread thread2 = new Thread(threadDemo5,"thread2");
    thread1.start();
    thread2.start();
}

}

執行結果

執行的線程是=>thread1執行結果爲->1
執行的線程是=>thread1執行結果爲->2
執行的線程是=>thread1執行結果爲->3
執行的線程是=>thread1執行結果爲->4
執行的線程是=>thread1執行結果爲->5
執行的線程是=>thread1執行結果爲->6
執行的線程是=>thread1執行結果爲->7
執行的線程是=>thread1執行結果爲->8
執行的線程是=>thread1執行結果爲->9
執行的線程是=>thread1執行結果爲->10
執行的線程是=>thread2執行結果爲->11
執行的線程是=>thread2執行結果爲->12
執行的線程是=>thread2執行結果爲->13
執行的線程是=>thread2執行結果爲->14
執行的線程是=>thread2執行結果爲->15
執行的線程是=>thread2執行結果爲->16
執行的線程是=>thread2執行結果爲->17
執行的線程是=>thread2執行結果爲->18
執行的線程是=>thread2執行結果爲->19
執行的線程是=>thread2執行結果爲->20

同步方法塊,synchronized鎖的是括號裏的對象,每個線程要進入代碼塊前必須先獲取對象的的鎖,纔可執行。synchronized是一個隱式鎖,也是jvm內置的鎖,它會自動加鎖和解鎖,同時java的每個對象都可以作爲鎖。

普通同步方法

public class ThreadDemo6 implements Runnable {

private int count = 0;

@Override
public void run() {
    say();
}

private synchronized void say(){
    for (int i = 0; i < 10; ++i){
        count++;
        System.out.println("現在執行的線程執行=>" + Thread.currentThread().getName() + "結果爲->" + count);
    }
    try {
        Thread.sleep(5000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

public static void main(String[] args) {
    ThreadDemo6 threadDemo6 = new ThreadDemo6();
    Thread thread1 = new Thread(threadDemo6,"Thread-1");
    Thread thread2 = new Thread(threadDemo6,"Thread-2");
    thread1.start();
    thread2.start();
}

}

執行結果

現在執行的線程執行=>Thread-1結果爲->1
現在執行的線程執行=>Thread-1結果爲->2
現在執行的線程執行=>Thread-1結果爲->3
現在執行的線程執行=>Thread-1結果爲->4
現在執行的線程執行=>Thread-1結果爲->5
現在執行的線程執行=>Thread-1結果爲->6
現在執行的線程執行=>Thread-1結果爲->7
現在執行的線程執行=>Thread-1結果爲->8
現在執行的線程執行=>Thread-1結果爲->9
現在執行的線程執行=>Thread-1結果爲->10
/停頓5秒/
現在執行的線程執行=>Thread-2結果爲->11
現在執行的線程執行=>Thread-2結果爲->12
現在執行的線程執行=>Thread-2結果爲->13
現在執行的線程執行=>Thread-2結果爲->14
現在執行的線程執行=>Thread-2結果爲->15
現在執行的線程執行=>Thread-2結果爲->16
現在執行的線程執行=>Thread-2結果爲->17
現在執行的線程執行=>Thread-2結果爲->18
現在執行的線程執行=>Thread-2結果爲->19
現在執行的線程執行=>Thread-2結果爲->20

普通同步方法,通過例子可以知道他是一個對象鎖,線程1未釋放鎖,線程2只能被動等待,改下代碼

public static void main(String[] args) {

    Thread thread1 = new Thread(new ThreadDemo6(),"Thread-1");
    Thread thread2 = new Thread(new ThreadDemo6(),"Thread-2");
    thread1.start();
    thread2.start();
}

執行結果

現在執行的線程執行=>Thread-2結果爲->1
現在執行的線程執行=>Thread-2結果爲->2
現在執行的線程執行=>Thread-2結果爲->3
現在執行的線程執行=>Thread-2結果爲->4
現在執行的線程執行=>Thread-2結果爲->5
現在執行的線程執行=>Thread-2結果爲->6
現在執行的線程執行=>Thread-2結果爲->7
現在執行的線程執行=>Thread-2結果爲->8
現在執行的線程執行=>Thread-2結果爲->9
現在執行的線程執行=>Thread-2結果爲->10
現在執行的線程執行=>Thread-1結果爲->1
現在執行的線程執行=>Thread-1結果爲->2
現在執行的線程執行=>Thread-1結果爲->3
現在執行的線程執行=>Thread-1結果爲->4
現在執行的線程執行=>Thread-1結果爲->5
現在執行的線程執行=>Thread-1結果爲->6
現在執行的線程執行=>Thread-1結果爲->7
現在執行的線程執行=>Thread-1結果爲->8
現在執行的線程執行=>Thread-1結果爲->9
現在執行的線程執行=>Thread-1結果爲->10
停頓。。

不是同一個對象鎖,所以線程1和線程2不存在鎖的互斥,並且不存在共享資源count變量,所以多個線程訪問的必須是同一個對象,鎖纔會變得有意義。

靜態同步方法

public class ThreadDemo6 implements Runnable {

private static int count = 0;

@Override
public void run() {
    say();
}

private static synchronized void say(){
    for (int i = 0; i < 10; ++i){
        count++;
        System.out.println("現在執行的線程執行=>" + Thread.currentThread().getName() + "結果爲->" + count);
    }
    try {
        Thread.sleep(5000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}
public static void main(String[] args) {
    Thread thread1 = new Thread(new ThreadDemo6(),"Thread-1");
    Thread thread2 = new Thread(new ThreadDemo6(),"Thread-2");
    thread1.start();
    thread2.start();
}

}

執行結果

現在執行的線程執行=>Thread-1結果爲->1
現在執行的線程執行=>Thread-1結果爲->2
現在執行的線程執行=>Thread-1結果爲->3
現在執行的線程執行=>Thread-1結果爲->4
現在執行的線程執行=>Thread-1結果爲->5
現在執行的線程執行=>Thread-1結果爲->6
現在執行的線程執行=>Thread-1結果爲->7
現在執行的線程執行=>Thread-1結果爲->8
現在執行的線程執行=>Thread-1結果爲->9
現在執行的線程執行=>Thread-1結果爲->10
/停頓/
現在執行的線程執行=>Thread-2結果爲->11
現在執行的線程執行=>Thread-2結果爲->12
現在執行的線程執行=>Thread-2結果爲->13
現在執行的線程執行=>Thread-2結果爲->14
現在執行的線程執行=>Thread-2結果爲->15
現在執行的線程執行=>Thread-2結果爲->16
現在執行的線程執行=>Thread-2結果爲->17
現在執行的線程執行=>Thread-2結果爲->18
現在執行的線程執行=>Thread-2結果爲->19
現在執行的線程執行=>Thread-2結果爲->20

即使他們是不同的對象,但執行的都是一個類的方法,在執行同步靜態方法時,爭搶的是類鎖,這也是和非靜態同步方法所區別開來。因爲他們是兩個不同的鎖,一個是對象鎖,一個是類鎖。所以,在代碼中,一個線程可以同時搶有對象鎖,類鎖。

 monitor和monitorexit
Java的互斥鎖是如何的實現的,javap -verbose ThreadDemo3.class 看下字節碼子令。

同步代碼塊

    

非靜態方法同步

靜態方法同步

同步塊中monitor被佔用就處於鎖定狀態,其他本次搶鎖失敗的線程將會放入Wait Set等待同步隊列中進行等待,佔用鎖的線程執行完同步塊並且釋放鎖後將會通知放入同步隊列的的其他線程,通知他們,我釋放鎖了趕緊來搶吧!而相對於普通的靜態同步方法和非靜態同步方法,常量池匯中多了ACC_SYNCHRONIZED標記,方法調用就會去檢查是不是有這個標記如果有,jvm就會要求線程在調用前先請求鎖,但無論哪種實現,在實質上還是通過對象相關聯的的monitor獲取的。

而monitor是什麼哪?它是每個對象創建之後都會在jvm內部維護一個與之對應Monitor(監視器鎖)也有人叫管程反正都是一個東西,可以理解爲每個對象天生都有一把看不見的鎖我們叫他monitor鎖,而每個線程會有一個可用的MR(Monitor Record)列表,還有一個全局可用列表,每一個被鎖住的對象都會和一個MR相關聯,並且對象monitor中會有一個owner字段存放佔用該鎖線程唯一標識,表示這個鎖已經被哪個線程佔用,synchronized就是基於進入與退出Monitor對象實現方法與代碼塊同步,而監視器鎖的實現哪是依賴底層操作系統Mutex lock(互斥鎖),它是一個重量級鎖,每次從用戶態切換到內的態的資源消耗是比較大的,也因此從jdk1.6後,java對synchronized進行了優化,從一開始的無鎖狀態->偏向鎖狀態->輕量級鎖狀態->重量級鎖狀態,並且這個狀態是不可逆的。

jvm加鎖過程

 對象內存結構
上文說過每個Java對象都是天生的鎖,存放在Java的對象頭中,對象頭包含三個區域,對象頭,實例數據,補齊填充

第一部分是存儲對象自身運行時的數據,哈希碼,GC,偏向時間戳,保存對象的分代年齡,鎖狀態標誌,偏向鎖線程id,線程持有的鎖,如果是數組還需要一塊區域存放數組大小,class的對象指針是虛擬機通過他確定這個對象是哪個類的實例,我們平時getClass直接獲取類就跟這個有關,官方稱這部分爲Mark Word,第二部分略過,第三部分規定對象的大小必須是8字節的整數倍,至於爲什麼,lz沒去深究暫時不知道。我們重點關注是Mark Word的鎖標誌位,所以鎖的狀態是保存在對象頭中的,至於偏向狀態,篇幅有限,下節在談。

鎖的粗化和消除
鎖的粗化

鎖帶來性能開銷是很大的,爲了保證多線程的併發操作,通常會要求每個線程持有鎖的時間越短越好,但如果遇到一連串對同一把鎖進行請求和釋放的操作,jvm會進行優化智能的把鎖操作的整合成一個較大同步塊,從而減少了對鎖的頻繁申請和釋放提高性能。

public class ThreadDemo7 implements Runnable {

public void test(){
    synchronized (this){
        System.out.println(1111);
    }
    synchronized (this){
        System.out.println(222);
    }
    synchronized (this){
        System.out.println(333);
    }
}
public static void main(String[] args) {
    ThreadDemo7 threadDemo7 = new ThreadDemo7();
    Thread thread = new Thread(threadDemo7);
    thread.start();
}

@Override
public void run() {
    test();
}

}

鎖的消除

我們設置了同步塊,在字節碼中也發現了monitorenter和monitorexit,至少看上去有鎖的獲取和釋放過程,但執行的結果與我們預測的風馬牛不相及。

public class ThreadDemo8 implements Runnable {

private static int count = 0;
@Override
public void run() {
    synchronized (new Object()){
        count++;
        System.out.println("鎖的消除...=>"  + Thread.currentThread().getName() + "值=>" + count);
    }
}

public static void main(String[] args) {
    ThreadDemo8 threadDemo8 = new ThreadDemo8();
    for (int i = 0; i < 10; ++i){
        Thread thread = new Thread(threadDemo8);
        thread.start();
    }
}

}

執行結果

鎖的消除...=>Thread-6值=>4
鎖的消除...=>Thread-4值=>2
鎖的消除...=>Thread-5值=>4
鎖的消除...=>Thread-0值=>4
鎖的消除...=>Thread-1值=>6
鎖的消除...=>Thread-2值=>6
鎖的消除...=>Thread-3值=>7
鎖的消除...=>Thread-9值=>8
鎖的消除...=>Thread-7值=>9
鎖的消除...=>Thread-8值=>10

這是因爲jit在編譯代碼時,使用了逃逸分析的技術,判斷程序中的使用鎖的對象是否被其他線程使用,如果只被一個線程使用,這個同步代碼就不會生成synchronized鎖標識的鎖申請和釋放的機器碼,消除了鎖的使用流程。所以,並不是所有的實例對象都存放在堆區,如果發生線程逃逸行爲,將會存儲在線程棧上。

總結
鎖的重入和鎖膨脹升級,在後期在慢慢整理。

參考

https://blog.csdn.net/axiaoboge/article/details/84335452

https://www.cnblogs.com/xrq730/p/4853578.html

原文地址https://www.cnblogs.com/dslx/p/12787683.html

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