燃燒吧!我的併發之魂--synchronized

張風捷特烈發表於2018-12-29

零、前言

經歷了兩個月的沉澱,感覺整體能力有所提升,最近除了年終總結也沒有什麼打算了
高併發這塊一致是我的心病,在這年尾,抽刀,奮力一擊吧
雖然會用執行緒,但是總感覺有很多地方讓我挺煩心,比如併發和那兩個關鍵字
曾經三次想要突破掉多執行緒,但都失敗了,只好暫時離開,現在的我感覺應該可以了
本文按照慕課網免費課程敲的,同時也加入了我大量的思考和繪圖,希望對你有所幫助


一、多執行緒的簡單回顧

1.入門級

下面WhatIsWrong實現Runnable,並提供一個靜態例項物件計時器i
run方法讓i自加10W次,下面的結果是多少?

public class WhatIsWrong implements Runnable {
    static WhatIsWrong instance = new WhatIsWrong();
    static int i = 0;
    public static void main(String[] args) {
        Thread thread_1 = new Thread(instance);
        Thread thread_2 = new Thread(instance);
        thread_1.start();
        thread_2.start();
        System.out.println(i);
    }
    @Override
    public void run() {
        for (int j = 0; j < 100000; j++) {
            i++;
        }
    }
}
複製程式碼

答案是0,簡單講一下:main中程式碼順序執行雖然執行緒1,2都開啟了,
但是程式還是順序執行的,會立刻走System.out.println(i);,實際看來run方法要慢一些

簡單分析.png


2.如何讓列印在兩個執行緒完成後才呼叫

兩個方法:1)讓主執行緒先睡一會、2)使用執行緒物件的join方法
總之就是推遲System.out.println(i);的執行時間

2.1:讓主執行緒先睡一會

這個方法很容易想到,但睡多久不好把握,一般小測試1s應該夠了

執行緒休眠.png

public class WhatIsWrong implements Runnable {
    static WhatIsWrong instance = new WhatIsWrong();
    static int i = 0;

    public static void main(String[] args) {
        Thread thread_1 = new Thread(instance);
        Thread thread_2 = new Thread(instance);
        thread_1.start();
        thread_2.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(i);
    }

    @Override
    public void run() {
        for (int j = 0; j < 100000; j++) {
            i++;
        }
    }
}
複製程式碼

2.2.join方法

正規的還是用join吧,他會讓該執行緒先行,使以System.out.println(i);被推後

執行緒join.png

public class WhatIsWrong implements Runnable {
    static WhatIsWrong instance = new WhatIsWrong();
    static int i = 0;

    public static void main(String[] args) {
        Thread thread_1 = new Thread(instance);
        Thread thread_2 = new Thread(instance);
        thread_1.start();
        thread_2.start();
        try {
            thread_1.join();
            thread_2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(i);
    }

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

複製程式碼

3.結果呢?
3.1下面是十次結果
137355  、 114412 、115381 、128482 、151021 、
109093 、  128610 、128144 、122390 、123746
複製程式碼

3.2從中能看出什麼?

1).每次執行結果都不一樣
2).它們都大於100000且小於200000


3.3為什麼

理論上兩個執行緒,每個執行緒加100000次,一共應該200000才對
想象一下這個場景:

有兩個神槍手對著靶子打(必中),每把槍有100000顆軟子彈
靶子有感應器和計數器,當軟子彈接觸靶子那一刻,計數器加1  
當兩個子彈同時接觸時,感應器有無法及時反應,只會被記錄一次,即計數器只+1
然後兩人瘋狂掃射,最後看靶子上計數器最終的數值  

可想而知最後計數器上的數應該是小於200000的,所以程式碼中也類似
兩個執行緒便是神槍手,run的時候開始同時瘋狂掃射,i便是靶子
複製程式碼

3.4:i++發生了什麼?

1)記憶體中讀取i的值,2)i=i+1,3)將結果寫回記憶體
i=9時,若執行緒2已經在第三步了,但還沒寫入記憶體。這時執行緒1進入,讀出i的值仍是9,
從而導致此次結束兩個結果都是10,這就是為什麼達不到200000的原因
這就相當於兩個神槍手同時開槍,靶子未及時反應而導致兩顆同彈

i++發生了什麼.png

不同步會出現什麼狀況.png

4.怎麼解決呢?

先看問題出在哪,是兩個人同時開槍對一個靶子
一個人是不能在同一時刻發出兩法子彈的,so,方法1:
準備兩個靶子,各自統計(像每個足球運動員一個足球一樣,10000個人怎麼辦,然並卵)
方法2:不允許兩個人同時開槍,這便是synchronized
神槍手1在掃射時,神射手2的槍自動鎖死,如果100條執行緒也是類似,某一刻只能一人開槍

public class WhatIsWrong implements Runnable {
    static WhatIsWrong instance = new WhatIsWrong();
    static int i = 0;

    public static void main(String[] args) {
        Thread thread_1 = new Thread(instance);
        Thread thread_2 = new Thread(instance);
        thread_1.start();
        thread_2.start();

        try {
            thread_1.join();
            thread_2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(i);//200000
    }

    @Override
    public synchronized void run() {//只需輕輕加一個synchronized即可
        for (int j = 0; j < 100000; j++) {
            i++;
        }
    }
}
複製程式碼

二、同步鎖

幾種鎖.png

0.測試程式碼(此時還未同步)

先看一下幹了什麼事:執行緒建立不說了,run方法中:
列印資訊-->當前執行緒睡三秒-->列印執行結束(如下圖)
根據時間線可以看出來列印結果(可以看出兩個人一起睡了,這還得了...)

非同步分析.png

public class SynObj_Block implements Runnable {
    static SynObj_Block instance = new SynObj_Block();

    public static void main(String[] args) {
        Thread thread_1 = new Thread(instance);
        Thread thread_2 = new Thread(instance);
        thread_1.start();
        thread_2.start();
        try {
            thread_1.join();
            thread_2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("All Finished");
    }

    @Override
    public void run() {
        System.out.println("物件鎖,程式碼塊形式--name:" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("執行結束,name:" + Thread.currentThread().getName());
    }
}
複製程式碼

列印結果:

物件鎖,程式碼塊形式--name:Thread-1
物件鎖,程式碼塊形式--name:Thread-0
執行結束,name:Thread-0
執行結束,name:Thread-1
All Finished
複製程式碼

1.物件鎖之同步程式碼塊鎖

上面說兩個執行緒一起睡了,執行緒1先睡,執行緒2進來也睡了,能忍嗎?不能忍!
快把哥的四十米大刀,不對,是大鎖拿來,在我睡覺前先把門鎖上
執行緒1進來睡,然後把門鎖上,執行緒2就進不來,只能等執行緒1把鎖開啟

同步程式碼塊.png


1.1:同步程式碼塊的新增

其他程式碼不變,就不貼了

@Override
public void run() {
    synchronized (this) {
        System.out.println("物件鎖,程式碼塊形式--name:" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("執行結束,name:" + Thread.currentThread().getName());
    }
}
複製程式碼

1.2:執行結果

可見執行緒1睡完,執行緒2才能進來睡

物件鎖,程式碼塊形式--name:Thread-0
執行結束,name:Thread-0
物件鎖,程式碼塊形式--name:Thread-1
執行結束,name:Thread-1
All Finished
複製程式碼

1.3:鎖物件

等等,這this是什麼鬼?--有點基礎的都知道是當前類物件

System.out.println(this);// top.toly.併發.SynObj_Block@77c89a74

同步程式碼塊synchronized()接收一個物件,該物件可任意指定:
Object lock = new Object();
synchronized (lock) {//TODO}  
新建一個物件也可以
複製程式碼

1.4:多把鎖

也會你會說:既然隨便一個物件都可以當做鎖物件,Java自己給內建個唄
還傳個引數,累不累人。等等,存在即合理,且看下面...
想一下如果一個房間兩張床,你上來把門鎖了,豈不是不合理?
那該怎麼辦?兩扇門,兩把不同的鎖唄(就像兩個人合租一間大房子一樣)
你可以根據圖中時間線好好想想(畫個圖也不是那麼容易的...且看且珍惜)

兩把鎖.png

/**
 * 作者:張風捷特烈
 * 時間:2018/12/28 0028:19:16
 * 郵箱:1981462002@qq.com
 * 說明:物件鎖--程式碼塊鎖
 */
public class SynObj_Block implements Runnable {
    static SynObj_Block instance = new SynObj_Block();
    Object lock1 = new Object();//第一把鎖
    Object lock2 = new Object();//第二把鎖
    public static void main(String[] args) {
        Thread thread_1 = new Thread(instance);
        Thread thread_2 = new Thread(instance);
        thread_1.start();
        thread_2.start();


        try {
            thread_1.join();
            thread_2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("All Finished");
    }

    @Override
    public void run() {
        synchronized (lock1) {
            System.out.println("lock1開始--name:" + Thread.currentThread().getName());
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("lock1結束,name:" + Thread.currentThread().getName());
        }
        synchronized (lock2) {
            System.out.println("lock2開始,程式碼塊形式--name:" + Thread.currentThread().getName());
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("lock2結束,name:" + Thread.currentThread().getName());
        }
    }
}
複製程式碼
物件鎖lock1,程式碼塊形式--name:Thread-0
lock1睡醒了,name:Thread-0
物件鎖lock1,程式碼塊形式--name:Thread-1
物件鎖lock2,程式碼塊形式--name:Thread-0
lock1睡醒了,name:Thread-1
lock2睡醒了,name:Thread-0
物件鎖lock2,程式碼塊形式--name:Thread-1
lock2睡醒了,name:Thread-1
All Finished
複製程式碼

有什麼好處?兩人合租房有什麼好處,多把鎖就有什麼好處。
可看出既完成任務,又減少了2秒,這也就兩個執行緒而已
如果百萬級的執行緒數,哪怕微小的效率提升都是有價值的


2.物件鎖之普通方法鎖

正如1.4所想:我就是想簡單的加個鎖,每次同步程式碼塊還有傳個物件,挺煩的
所以有一個叫方法鎖,什麼物件每個類都有?答案:this,方法鎖的物件預設是this

2.1:使用
/**
 * 作者:張風捷特烈
 * 時間:2018/12/28 0028:19:16
 * 郵箱:1981462002@qq.com
 * 說明:物件鎖--普通方法鎖
 */
public class SynObj_Method implements Runnable {
    static SynObj_Method instance = new SynObj_Method();

    public static void main(String[] args) {
        Thread thread_1 = new Thread(instance);
        Thread thread_2 = new Thread(instance);
        thread_1.start();
        thread_2.start();
        try {
            thread_1.join();
            thread_2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("All Finished");
    }

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

    public synchronized void sleep3ms() {
        System.out.println("方法鎖測試--name:" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("結束,name:" + Thread.currentThread().getName());
    }
}
複製程式碼

2.2:列印結果

和同步程式碼塊一致

方法鎖測試--name:Thread-0
結束,name:Thread-0
方法鎖測試--name:Thread-1
結束,name:Thread-1
All Finished
複製程式碼

2.3:如何證明方法鎖的鎖物件是this

你說this就this?何以見得?

@Override
public void run() {
    sleep3ms();
    synchronized (this){
        System.out.println("測試開始--name:" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("測試結束,name:" + Thread.currentThread().getName());
    }
}

public synchronized void sleep3ms() {
    System.out.println("方法鎖測試--name:" + Thread.currentThread().getName());
    try {
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("結束,name:" + Thread.currentThread().getName());
}
複製程式碼
方法鎖開始--name:Thread-0
方法鎖結束,name:Thread-0
同步程式碼塊測試開始--name:Thread-0
同步程式碼塊測試結束,name:Thread-0
方法鎖開始--name:Thread-1
方法鎖結束,name:Thread-1
同步程式碼塊測試開始--name:Thread-1
同步程式碼塊測試結束,name:Thread-1
All Finished
複製程式碼

加上this同步程式碼塊後:可見開始與結束兩兩配對
說明方法鎖和同步程式碼塊的this鎖是一把鎖,也就是隻有一扇門,必須一個一個睡


2.4:反證
@Override
public void run() {
    sleep3ms();
    synchronized (""){
        System.out.println("測試開始--name:" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("測試結束,name:" + Thread.currentThread().getName());
    }
}

public synchronized void sleep3ms() {
    System.out.println("方法鎖測試--name:" + Thread.currentThread().getName());
    try {
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("結束,name:" + Thread.currentThread().getName());
}
複製程式碼
方法鎖開始--name:Thread-0
方法鎖結束,name:Thread-0
方法鎖開始--name:Thread-1
同步程式碼塊測試開始--name:Thread-0
方法鎖結束,name:Thread-1
同步程式碼塊測試結束,name:Thread-0
同步程式碼塊測試開始--name:Thread-1
同步程式碼塊測試結束,name:Thread-1
All Finished
複製程式碼

如果鎖不是this,這裡簡單點用"",可見Thread-0的結束後
Thread-1的方法鎖開始和Thread-0的同步程式碼塊測試開始是同時列印出來的
說明有兩扇門,那兩把鎖不是同一把,也反向表明,非this會產生兩把鎖
綜上正反兩面,我們可以感受到方法鎖的鎖物件是this


3.類鎖之靜態方法鎖(static方法+synchronized)

說是類鎖,實質上是使用了Class物件當做鎖,非要較真的話,你可以把他看作物件鎖
Class物件有什麼特點:一個類可以有多個物件,但僅有一個Class物件
這就可以導致:類鎖只能在同一時刻被一個物件擁有


3.1.static方法+synchronized

普通方法+synchronized但是兩個不同的Runnable物件執行緒

public class Syn_Static_Method implements Runnable {
    static Syn_Static_Method instance1 = new Syn_Static_Method();
    static Syn_Static_Method instance2 = new Syn_Static_Method();
    public static void main(String[] args) {
        Thread thread_1 = new Thread(instance1);
        Thread thread_2 = new Thread(instance2);
        thread_1.start();
        thread_2.start();
        try {
            thread_1.join();
            thread_2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("All Finished");
    }
    @Override
    public void run() {
        sleep3ms();
    }
    public synchronized void sleep3ms() {
        System.out.println("靜態方法鎖開始--name:" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("靜態方法鎖開始,name:" + Thread.currentThread().getName());
    }
}
複製程式碼

好吧,用腳趾頭想想也知道互不影響
這相當於兩個人有兩個家,各自進各自的家睡覺天經地義
你加synchronized鎖你家的門管我什麼事,所以synchronized這時並沒用處

靜態方法鎖開始--name:Thread-1
靜態方法鎖開始--name:Thread-0
靜態方法鎖開始,name:Thread-0
靜態方法鎖開始,name:Thread-1
All Finished
複製程式碼

我們都知道static關鍵字修飾的方法、變數,是可以令於類名(Class物件)的
也就是不需要物件便可以執行,由static修飾的方法是不能用this物件的
這就是為什麼加一個static,鎖就不同了的原因,至於鎖是什麼,除了它的老大還有人選嗎?

/**
 * 作者:張風捷特烈
 * 時間:2018/12/28 0028:19:16
 * 郵箱:1981462002@qq.com
 * 說明:物件鎖--靜態方法鎖
 */
public class Syn_Static_Method implements Runnable {
    //同上...略
    public static synchronized void sleep3ms() {//我就輕輕加個static
       //同上...略
    }
}
複製程式碼
靜態方法鎖開始--name:Thread-0
靜態方法鎖開始,name:Thread-0
靜態方法鎖開始--name:Thread-1
靜態方法鎖開始,name:Thread-1
All Finished
複製程式碼

符合預期:這樣就將一個類給鎖起來了,只要是這個類的物件
都會生效,這也是它的優勢,也是static的本意:靜態,具有全域性控制力


4.類鎖之Class物件鎖

相當於把static+synchronized拆出來

/**
 * 作者:張風捷特烈
 * 時間:2018/12/28 0028:19:16
 * 郵箱:1981462002@qq.com
 * 說明:物件鎖--class鎖
 */
public class Syn_Class implements Runnable {
    static Syn_Class instance1 = new Syn_Class();
    static Syn_Class instance2 = new Syn_Class();

    public static void main(String[] args) {
        Thread thread_1 = new Thread(instance1);
        Thread thread_2 = new Thread(instance2);
        thread_1.start();
        thread_2.start();
        try {
            thread_1.join();
            thread_2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("All Finished");
    }

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

    public void sleep3ms() {
        synchronized (Syn_Class.class) {
            System.out.println("class鎖開始--name:" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("class鎖開始,name:" + Thread.currentThread().getName());
        }
    }
}
複製程式碼

class鎖開始--name:Thread-0
class鎖開始,name:Thread-0
class鎖開始--name:Thread-1
class鎖開始,name:Thread-1
All Finished
複製程式碼

5.現在回頭來看

synchronized:同步的

官宣:
同步方法支援一種簡單的策略來[防止執行緒干擾]和[記憶體一致性錯誤]:
如果一個物件變數對多個執行緒可見,則對它的所有讀寫都是通過同步方法完成的

民宣:
保證同一時刻最多隻一個執行緒執行該段程式碼,來保證併發安全
複製程式碼

三、多執行緒訪問方法的一些情況

感覺有點...麻煩

1.兩個執行緒訪問一個物件的普通同步方法
2.兩個執行緒訪問兩個物件的普通同步方法
3.兩個執行緒訪問靜態同步方法
4.兩個執行緒分別訪問普通同步方法和非同步方法
5.兩個執行緒分別訪問一個物件的不同普通同步方法
6.兩個執行緒分別訪問靜態同步和非靜態同步方法
方法丟擲異常後,會釋放鎖
複製程式碼

1.兩個執行緒訪問一個物件的普通同步方法

二-->2中的例子:執行緒1,2訪問一個物件instance的同步方法:sleep3ms
同一個物件,需要等待鎖的釋放,才能進入普通同步方法


2.兩個執行緒訪問兩個物件的普通同步方法

二-->3-->3.1中第一個小例子(用腳趾頭想的那個)
同一類的兩個不同物件的普通同步方法,對於兩個執行緒而言,同步是無用的


3.兩個執行緒訪問靜態同步方法

二-->3-->3.1第二個小例子,輕輕加了個static
由於靜態同步方法的鎖是class,鎖對該類的所有物件都有效


4.兩個執行緒分別訪問普通同步方法和非同步方法

執行緒2的方法沒加鎖(非同步方法),就進來睡了唄,也沒什麼特別的
注意兩頭幾乎同時執行,測試了幾次,兩頭的先後順序不定

兩個執行緒分別訪問普通同步方法和非同步方法.png

/**
 * 作者:張風捷特烈
 * 時間:2018/12/29 0029:11:31
 * 郵箱:1981462002@qq.com
 * 說明:兩個執行緒分別訪問普通同步方法和非同步方法
 */
public class SynOrNot implements Runnable {
    static SynOrNot instance = new SynOrNot();

    public static void main(String[] args) {
        Thread thread_1 = new Thread(instance);
        Thread thread_2 = new Thread(instance);
        thread_1.start();
        thread_2.start();
        try {
            thread_1.join();
            thread_2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("All Finished");
    }

    @Override
    public void run() {
        if (Thread.currentThread().getName().equals("Thread-0")) {
            sleep3msSync();
        } else {
            sleep3msCommon();
        }
    }

    public void sleep3msCommon() {
        System.out.println("非同步方法開始--name:" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("非同步方法結束,name:" + Thread.currentThread().getName());
    }

    public synchronized void sleep3msSync() {
        System.out.println("同步方法開始--name:" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("同步方法結束,name:" + Thread.currentThread().getName());
    }
}
複製程式碼
同步方法開始--name:Thread-0
非同步方法開始--name:Thread-1
同步方法結束,name:Thread-0
非同步方法結束,name:Thread-1
All Finished
複製程式碼

5.兩個執行緒分別訪問一個物件的不同普通同步方法

由於普通同步方法是this鎖,所以對不同普通同步方法鎖是一致的,都生效

兩個執行緒分別訪問一個物件的不同普通同步方法.png

/**
 * 作者:張風捷特烈
 * 時間:2018/12/29 0029:11:31
 * 郵箱:1981462002@qq.com
 * 說明:兩個執行緒分別訪問一個物件的不同普通同步方法
 */
public class SynOfTwo implements Runnable {
    static SynOfTwo instance = new SynOfTwo();
    public static void main(String[] args) {
        Thread thread_1 = new Thread(instance);
        Thread thread_2 = new Thread(instance);
        thread_1.start();
        thread_2.start();
        try {
            thread_1.join();
            thread_2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("All Finished");
    }

    @Override
    public void run() {
        if (Thread.currentThread().getName().equals("Thread-0")) {
            sleep3msSync1();
        } else {
            sleep3msSync2();
        }
    }

    public synchronized void sleep3msSync2() {
        System.out.println("sleep3msSync2方法開始--name:" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("sleep3msSync2結束,name:" + Thread.currentThread().getName());
    }

    public synchronized void sleep3msSync1() {
        System.out.println("sleep3msSync1開始--name:" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("sleep3msSync1結束,name:" + Thread.currentThread().getName());
    }
}
複製程式碼
sleep3msSync1開始--name:Thread-0
sleep3msSync1結束,name:Thread-0
sleep3msSync2方法開始--name:Thread-1
sleep3msSync2結束,name:Thread-1
All Finished
複製程式碼

6.兩個執行緒分別訪問靜態同步和普通同步方法

不測試都知道:一個是class鎖,一個是this鎖,鎖不同,不生效
在第5個的基礎上加上static關鍵字,其餘不變,結果不出所料

兩個執行緒分別訪問靜態同步和普通同步方法.png

public static synchronized void sleep3msSync2() {
複製程式碼
sleep3msSync1開始--name:Thread-0
sleep3msSync2方法開始--name:Thread-1
sleep3msSync2結束,name:Thread-1
sleep3msSync1結束,name:Thread-0
All Finished
複製程式碼

7.丟擲異常後,釋放鎖

可以看出執行緒1拋異常後,執行緒2是可以正常執行的(說明執行緒1的鎖已經被釋放)
就像執行緒1在睡覺,睡著睡著仙逝了,房東(JVM)會把它抬走,把鎖給下一個人,繼續睡...
在第5個的程式碼上稍微修改:int a=1/0;//異常

執行緒1出現異常.png

public synchronized void sleep3msSync1() {
    System.out.println("sleep3msSync1開始--name:" + Thread.currentThread().getName());
    try {
        Thread.sleep(3000);
        int a=1/0;//異常
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("sleep3msSync1結束,name:" + Thread.currentThread().getName());
}
複製程式碼
sleep3msSync1開始--name:Thread-0
Exception in thread "Thread-0" java.lang.ArithmeticException: / by zero
sleep3msSync2方法開始--name:Thread-1
	at top.toly.併發.SynOfError.sleep3msSync1(SynOfError.java:54)
	at top.toly.併發.SynOfError.run(SynOfError.java:33)
	at java.base/java.lang.Thread.run(Thread.java:844)
sleep3msSync2結束,name:Thread-1
All Finished
複製程式碼

一把鎖只能由一個執行緒獲取,沒拿到鎖的執行緒必須等待
不同的鎖之間互不影響(相當於進不同的門,互不干擾,無需等待)
無論正常執行還是丟擲異常,都會釋放鎖


8、synchronized的性質
可重入:同一執行緒外層函式獲取鎖之後,內層函式可以直接再次獲取該鎖
好處:避免死鎖,提高封裝性
粒度:執行緒範圍  
即synchronized修飾的同步方法內部`並非只能`呼叫同步方法
複製程式碼
不可中斷:比如我執行緒1要小睡個十萬年,那執行緒2就要在門等上十萬年(想走都不行)。
複製程式碼

四、Java記憶體模型(JMM--Java Memory Model)

1.Java記憶體模型的概念

描述Java程式中各變數(執行緒共享變數)的訪問規則,
即在JVM中將變數儲存到記憶體和從記憶體中讀取變數的底層細節

1.所有的變數都儲存在主記憶體中,
2.每條執行緒都有自己獨立的工作記憶體。其儲存該執行緒用到的變數副本(主記憶體變數拷貝)。

規定:
[1]執行緒對變數的所有操作都必須在工作記憶體中進行,而不能直接讀寫主記憶體。
[2]執行緒無法直接訪問非己方工作記憶體中的變數,執行緒間變數值的傳遞需要間接通過主記憶體。
複製程式碼

JMM--java記憶體模型.png


2.如何:執行緒1的修改被執行緒2看到

1.工作記憶體1操作共享變數a後重新整理到主記憶體
2.然後執行緒2從主記憶體中讀取共享變數a值並拷貝到自己的工作記憶體

共享變數可見性.png


3、synchronized實現可見性

鎖定的執行緒1所做的任何修改都要在釋放鎖之前從工作記憶體重新整理到主記憶體
執行緒2拿到鎖時從主記憶體中拷貝需要的變數到自己的工作記憶體(從而實現共享變數的可見)


4、缺陷:
效率低:
鎖的釋放情況少(只能自動釋放,或異常)
不能中斷等待鎖的執行緒

不靈活:
加鎖和釋放鎖的時機單一,每個鎖只有單一的條件
無法知道釋放成功獲取鎖
複製程式碼

5.注意點
鎖物件不能為空,作用域不宜過大,避免死鎖
|---鎖物件的資訊是放在物件頭中,所以不能為空
|---作用域過大,導致序列執行的程式碼變多,效率下降

Lock還是synchronized
|---儘量使用併發包裡的原子類
|---synchronized能完成的儘量不去Lock
|---確實需要中斷等待、靈活開解鎖或Condition可以使用Lock鎖

多執行緒訪問同步方法的幾種情況
複製程式碼
死鎖簡單演示
/**
 * 作者:張風捷特烈
 * 時間:2018/12/29 0029:11:31
 * 郵箱:1981462002@qq.com
 * 說明:死鎖簡單演示
 */
public class SynKill implements Runnable {
    static SynKill instance1 = new SynKill();
    static SynKill instance2 = new SynKill();
    public static void main(String[] args) {
        Thread thread_1 = new Thread(instance1);
        Thread thread_2 = new Thread(instance1);
        thread_1.start();
        thread_2.start();
        try {
            thread_1.join();
            thread_2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("All Finished");
    }
    @Override
    public void run() {
        sleep3msSync1();
        sleep3msSync2();
    }

    public void sleep3msSync2() {
        synchronized (instance1) {
            System.out.println("sleep3msSync2方法開始--name:" + Thread.currentThread().getName());
            synchronized (instance2) {
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("sleep3msSync2結束,name:" + Thread.currentThread().getName());
       }
    }

    public static synchronized void sleep3msSync1() {
        synchronized (instance2) {
            System.out.println("sleep3msSync1方法開始--name:" + Thread.currentThread().getName());
            synchronized (instance1) {
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("sleep3msSync1結束,name:" + Thread.currentThread().getName());
        }
    }
}
複製程式碼

死鎖.png


六、synchronized原理簡述

1.定義一個類,其中用一個同步方法
public class Decode {

    private Object obj = new Object();

    public void say(Thread thread) {
        synchronized (obj){

        }
    }

}
複製程式碼

2.反編譯(含同步方法的類):
I:\Java\Base\Thinking\src\top\toly\併發>javac -encoding utf-8 Decode.java

I:\Java\Base\Thinking\src\top\toly\併發>javap -verbose Decode.class
Classfile /I:/Java/Base/Thinking/src/top/toly/併發/Decode.class
  Last modified 2018年12月29日; size 465 bytes
  MD5 checksum 732654b709aafd523b08c943dcb1f235
  Compiled from "Decode.java"
public class top.toly.併發.Decode
  minor version: 0
  major version: 54
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #4                          // top/toly/併發/Decode
  super_class: #2                         // java/lang/Object
  interfaces: 0, fields: 1, methods: 2, attributes: 1
Constant pool:
   #1 = Methodref          #2.#18         // java/lang/Object."<init>":()V
   #2 = Class              #19            // java/lang/Object
   #3 = Fieldref           #4.#20         // top/toly/併發/Decode.obj:Ljava/lang/Object;
   #4 = Class              #21            // top/toly/併發/Decode
   #5 = Utf8               obj
   #6 = Utf8               Ljava/lang/Object;
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               say
  #12 = Utf8               (Ljava/lang/Thread;)V
  #13 = Utf8               StackMapTable
  #14 = Class              #22            // java/lang/Thread
  #15 = Class              #23            // java/lang/Throwable
  #16 = Utf8               SourceFile
  #17 = Utf8               Decode.java
  #18 = NameAndType        #7:#8          // "<init>":()V
  #19 = Utf8               java/lang/Object
  #20 = NameAndType        #5:#6          // obj:Ljava/lang/Object;
  #21 = Utf8               top/toly/併發/Decode
  #22 = Utf8               java/lang/Thread
  #23 = Utf8               java/lang/Throwable
{
  public top.toly.併發.Decode();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: new           #2                  // class java/lang/Object
         8: dup
         9: invokespecial #1                  // Method java/lang/Object."<init>":()V
        12: putfield      #3                  // Field obj:Ljava/lang/Object;
        15: return
      LineNumberTable:
        line 9: 0
        line 11: 4

  public void say(java.lang.Thread);
    descriptor: (Ljava/lang/Thread;)V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=2
         0: aload_0
         1: getfield      #3                  // Field obj:Ljava/lang/Object;
         4: dup
         5: astore_2
         6: monitorenter  <---------------monitorenter
         7: aload_2
         8: monitorexit   <---------------monitorexit
         9: goto          17
        12: astore_3
        13: aload_2
        14: monitorexit  <---------------monitorexit
        15: aload_3
        16: athrow
        17: return
      Exception table:
         from    to  target type
             7     9    12   any
            12    15    12   any
      LineNumberTable:
        line 14: 0
        line 16: 7
        line 17: 17
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 12
          locals = [ class top/toly/併發/Decode, class java/lang/Thread, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4
}
SourceFile: "Decode.java"

複製程式碼

3.如果將同步程式碼塊去掉,再反編譯
I:\Java\Base\Thinking\src\top\toly\併發>javap -verbose Decode.class
Classfile /I:/Java/Base/Thinking/src/top/toly/併發/Decode.class
  Last modified 2018年12月29日; size 331 bytes
  MD5 checksum 7963d00f1f781bc47a9700c548692617
  Compiled from "Decode.java"
public class top.toly.併發.Decode
  minor version: 0
  major version: 54
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #4                          // top/toly/併發/Decode
  super_class: #2                         // java/lang/Object
  interfaces: 0, fields: 1, methods: 2, attributes: 1
Constant pool:
   #1 = Methodref          #2.#15         // java/lang/Object."<init>":()V
   #2 = Class              #16            // java/lang/Object
   #3 = Fieldref           #4.#17         // top/toly/併發/Decode.obj:Ljava/lang/Object;
   #4 = Class              #18            // top/toly/併發/Decode
   #5 = Utf8               obj
   #6 = Utf8               Ljava/lang/Object;
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               say
  #12 = Utf8               (Ljava/lang/Thread;)V
  #13 = Utf8               SourceFile
  #14 = Utf8               Decode.java
  #15 = NameAndType        #7:#8          // "<init>":()V
  #16 = Utf8               java/lang/Object
  #17 = NameAndType        #5:#6          // obj:Ljava/lang/Object;
  #18 = Utf8               top/toly/併發/Decode
{
  public top.toly.併發.Decode();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: new           #2                  // class java/lang/Object
         8: dup
         9: invokespecial #1                  // Method java/lang/Object."<init>":()V
        12: putfield      #3                  // Field obj:Ljava/lang/Object;
        15: return
      LineNumberTable:
        line 9: 0
        line 11: 4

  public void say(java.lang.Thread);
    descriptor: (Ljava/lang/Thread;)V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=0, locals=2, args_size=2
         0: return
      LineNumberTable:
        line 14: 0
}
SourceFile: "Decode.java"

複製程式碼

兩次的對比可以看出:obj物件上的東西有點不一樣
加了synchronized程式碼塊的,obj物件頭會有monitorentermonitorexit
注意是加鎖時使用的物件obj的物件頭


4.monitorentermonitorexit
monitorenter次數為0時:若執行緒1進入,monitorenter次數+1,執行緒1成為該Monitor的所有者
若此時執行緒2進入,由於Monitor的所有者非執行緒2,執行緒2只能等待,直到monitorenter次數為0

若執行緒1進入同步方法後,又呼叫了一次其他方法,則monitorenter次數+1,方法退出時-1(可重入)
當monitorenter次數為0,說明:執行緒1的該同步方法執行完畢,將工作記憶體重新整理到主記憶體,並釋放鎖  
這時monitorenter次數為0,執行緒2允許進入,monitorenter次數+1,執行緒2成為該Monitor的所有者
複製程式碼

更深的東西以後慢慢來吧,先了解個執行緒同步的大概,併發的內功也不是一朝一夕能成的


後記:捷文規範

1.本文成長記錄及勘誤表
專案原始碼 日期 備註
V0.1 2018-12-29 燃燒吧!我的併發之魂--synchronized
2.更多關於我
筆名 QQ 微信 愛好
張風捷特烈 1981462002 zdl1994328 語言
我的github 我的簡書 我的掘金 個人網站
3.宣告

1----本文由張風捷特烈原創,轉載請註明
2----歡迎廣大程式設計愛好者共同交流
3----個人能力有限,如有不正之處歡迎大家批評指證,必定虛心改正
4----看到這裡,我在此感謝你的喜歡與支援


icon_wx_200.png

相關文章