天天看點

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

停頓。。

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

靜态同步方法

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();
}           

/停頓/

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

 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