天天看點

JUC并發程式設計(3)---八鎖現象,讓你徹底了解鎖

目錄

    • 前言
    • 八鎖現象
    • 一、synchronized鎖的對象是方法的調用者
    • 二、static在類加載就有Class模闆,鎖Class

前言

我們都知道,隻要有并發,就必然要鎖來保證線程安全,那什麼是鎖呢?這裡将舉8個例子,對應鎖的8個問題,讓你徹底搞懂鎖的原理與使用

八鎖現象

一、synchronized鎖的對象是方法的調用者

上面标題是先抛出的結論,我們一步步來得到這個結論

案例:電話類,有打電話和發短信兩個功能,兩個線程分别執行發短信和打電話,我們看下面代碼,是先列印發短信還是先列印打電話?

注:代碼中TimeUnit.SECONDS.sleep(1),這是JUC包中的睡眠方法,類似于多線程中的Thread.sleep(1000)

package com.yx.lock8;

import java.util.concurrent.TimeUnit;

//1、标準情況下,兩個線程先列印發短信,還是打電話?
public class test1 {
    public static void main(String[] args) {

        Phone1 phone = new Phone1();

        new Thread(()->{
            phone.sendMsg();
        },"A").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(()->{
            phone.call();
        },"B").start();
    }
}

class Phone1{

    //發短信
    public synchronized void sendMsg(){
        System.out.println("發短信");
    }
    //打電話
    public synchronized void call(){
        System.out.println("打電話");
    }
}
           
JUC并發程式設計(3)---八鎖現象,讓你徹底了解鎖

這裡運作結果是先發短信,為什麼呢?可能有人會覺得線程A先調用,是以先執行發短信,這是錯誤的!!!我們可以再看一個例子對比一下,再得出原因

我們在發短信加一個3秒睡眠,那列印結果是什麼?

package com.yx.lock8;

import java.util.concurrent.TimeUnit;

//2、發短信加一個3秒睡眠,兩個線程先列印發短信,還是打電話?
public class test2 {
    public static void main(String[] args) {

        Phone2 phone = new Phone2();

        new Thread(()->{
            phone.sendMsg();
        },"A").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(()->{
            phone.call();
        },"B").start();
    }
}

class Phone2{

    //發短信
    public synchronized void sendMsg(){
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("發短信");
    }
    //打電話
    public synchronized void call(){
        System.out.println("打電話");
    }
}
           
JUC并發程式設計(3)---八鎖現象,讓你徹底了解鎖

結果還是發短信,打電話。并且發短信等待了3秒才列印。現在我們可以回答上面留下的問題了,為什麼總是先發短信,再打電話。因為這是有鎖的存在!!!因為synchronized鎖的對象是方法的調用者,這兩個方法調用者都是phone這個對象,是以這兩個方法用的是同一把鎖,誰先拿到鎖誰就先執行,而phone.sendMsg()先加載執行,就先拿到鎖,是以call()方法隻能等待發短信用完鎖(釋放鎖),才輪到call()用鎖。就好像小餐館排隊如廁,隻有一個坑位(一把鎖),先進去關門,鎖上門。後面的必須等前面如廁完,開鎖并開門,才能用廁所。

我們再來,如果加一個普通方法hello,沒有synchronized關鍵字修飾,然後線程B調用呢

package com.yx.lock8;

import java.util.concurrent.TimeUnit;

//3、加一個普通方法hello,列印結果是?
public class test3 {
    public static void main(String[] args) {

        Phone3 phone = new Phone3();

        new Thread(()->{
            phone.sendMsg();
        },"A").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(()->{
            phone.hello();
        },"B").start();
    }
}

class Phone3{

    //發短信
    public synchronized void sendMsg(){
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("發短信");
    }
    //打電話
    public synchronized void call(){
        System.out.println("打電話");
    }

    public void hello(){
        System.out.println("hello");
    }
}
           
JUC并發程式設計(3)---八鎖現象,讓你徹底了解鎖

結果是先列印hello,為什麼呢?因為根據synchronized鎖的對象是方法的調用者這句話,這兩個方法就不是一個鎖,hello方法就沒有鎖,是以不會受到鎖的影響,自然因為發短信方法休眠3秒,就提前線上程B調用時就列印了。是不是用到同一把鎖才是關鍵。

我們再舉第4個案例,兩個對象,一個發短信,一個打電話,這樣結果是什麼呢?

package com.yx.lock8;

import java.util.concurrent.TimeUnit;

//2、兩個對象,分别調用,怎麼列印呢?
public class test4 {
    public static void main(String[] args) {

        //兩個對象
        Phone4 phone1 = new Phone4();
        Phone4 phone2 = new Phone4();

        new Thread(()->{
            phone1.sendMsg();
        },"A").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(()->{
            phone2.call();
        },"B").start();
    }
}

class Phone4{

    //發短信
    public synchronized void sendMsg(){
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("發短信");
    }
    //打電話
    public synchronized void call(){
        System.out.println("打電話");
    }
}
           
JUC并發程式設計(3)---八鎖現象,讓你徹底了解鎖

這我們應該就能脫口而出了,先打電話後發短信,根據synchronized鎖的對象是方法的調用者來看,兩個方法屬于兩個不同對象,自然就是兩把鎖,就相當于可以進兩個不同的廁所,互不影響呀。是以才會先打電話,然後3秒後列印發短信。

二、static在類加載就有Class模闆,鎖Class

同樣我們先說出結論,後面舉例一步步論證

package com.yx.lock8;

import java.util.concurrent.TimeUnit;

//5、兩方法都加static,列印結果是?
public class test5{
    public static void main(String[] args) {

        Phone5 phone = new Phone5();

        new Thread(()->{
            phone.sendMsg();
        },"A").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(()->{
            phone.call();
        },"B").start();
    }
}

//Phone5的class全局唯一
class Phone5{

    //發短信
    public static synchronized void sendMsg(){
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("發短信");
    }
    //打電話
    public static synchronized void call(){
        System.out.println("打電話");
    }
}
           
JUC并發程式設計(3)---八鎖現象,讓你徹底了解鎖

先列印發短信,為什麼呢?這是因為synchronized鎖的對象是方法的調用者這個原因嗎?不!!!這裡是因為涉及到反射,static在類加載時加載了反射得到對象的class,也就是Phone5對象的class,Phone5.class。那麼兩個方法都用static修飾,說明鎖的class相同。自然用的是同一把鎖,是以senMsg先調用的,就先上鎖執行,是以後面的線程B隻能等待,是以執行時等待3秒出現發短信,再出現打電話

我們再舉一個案例,如果在staic修飾基礎上,再執行個體化兩個對象分别調用兩個方法呢?

package com.yx.lock8;

import java.util.concurrent.TimeUnit;

//6、兩方法都加static,并且執行個體化兩個對象調用對應方法
public class test6{
    public static void main(String[] args) {

        Phone6 phone1 = new Phone6();
        Phone6 phone2 = new Phone6();

        new Thread(()->{
            phone1.sendMsg();
        },"A").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(()->{
            phone2.call();
        },"B").start();
    }
}

class Phone6{

    //發短信
    public static synchronized void sendMsg(){
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("發短信");
    }
    //打電話
    public static synchronized void call(){
        System.out.println("打電話");
    }
}
           
JUC并發程式設計(3)---八鎖現象,讓你徹底了解鎖

結果還是先列印發短信,後列印打電話,因為static在類加載時加載了反射得到對象的class,雖然執行個體化兩個對象,但是用的都是Phone6.class,是以用的是同一把鎖,自然線程A調用發短信先調用執行,線程B必須等待線程A釋放鎖。這裡就可以和之前執行個體化兩個對象但沒有加static做對比,執行個體化兩個對象屬于兩把鎖,這裡由于是鎖class,是以還是用同一把鎖。

我們再舉出一個例子,下面是第7個例子,一個普通不加staic方法,一個加staic方法,列印結果是?

package com.yx.lock8;

import java.util.concurrent.TimeUnit;

//7、一個不加staic方法,一個加staic方法,列印結果是?
public class test7{
    public static void main(String[] args) {

        Phone7 phone = new Phone7();

        new Thread(()->{
            phone.sendMsg();
        },"A").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(()->{
            phone.call();
        },"B").start();
    }
}

class Phone7{

    //發短信
    public static synchronized void sendMsg(){
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("發短信");
    }
    //打電話
    public synchronized void call(){
        System.out.println("打電話");
    }
}
           
JUC并發程式設計(3)---八鎖現象,讓你徹底了解鎖

相信大家都能知道結果了,沒錯,就是先打電話後發短信,為什麼呢?我相信基本都能說出來了吧,因為staic修飾鎖的是Class,沒有staic修飾鎖的是phone對象,這是兩把鎖,是以互不影響,不需要等待,是以先打電話,發短信sleep3秒才列印。

最後一個案例,大家直接猜猜結果

一個不加staic方法,一個加staic方法,兩個對象

package com.yx.lock8;

import java.util.concurrent.TimeUnit;

//8、一個普通不加staic方法,一個加staic方法,兩個對象,列印結果是?
public class test8{
    public static void main(String[] args) {

        Phone8 phone1 = new Phone8();
        Phone8 phone2 = new Phone8();

        new Thread(()->{
            phone1.sendMsg();
        },"A").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(()->{
            phone2.call();
        },"B").start();
    }
}

class Phone8{

    //發短信
    public static synchronized void sendMsg(){
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("發短信");
    }
    //打電話
    public synchronized void call(){
        System.out.println("打電話");
    }
}
           
JUC并發程式設計(3)---八鎖現象,讓你徹底了解鎖

結果是先打電話,因為雖然是兩個對象,但一個是鎖Class,一個是phone2對象,是兩把鎖,是以不用等待,互不影響。讀到這裡相信你對鎖有了更為深刻的了解。

我們做個總結:看線程執行調用先後順序,核心是看多個線程是否用的是同一把鎖,同一把鎖就要等待,不能插隊;用多個鎖就不用等待,相當于有多個坑位。并且synchronized和staic修飾是不同的,synchronized鎖的對象是方法的調用者,也就是修飾的方法對應調用者,而static鎖的是Class,無論執行個體化多少對象,隻要他們的類.Class相同,那麼就是同一把鎖。

繼續閱讀