Java——深刻理解鎖(邊邊奮鬥史)

2020-10-13 15:00:44

引入

今天我們將通過幾個場景來深刻的理解幾個問題。

如下:

  • 什麼是鎖?
  • 是誰的鎖?
  • 鎖的是誰?

場景1

import java.util.concurrent.TimeUnit;

/**
 * @ClassName test1
 * @Description
 * @Author SkySong
 * @Date 2020-10-11 18:13
 */
public class test1 {
    public static void main(String[] args) throws InterruptedException {
        Phone phone = new Phone();
        new Thread(()->{
            phone.sendMsm();
        },"A").start();
        //等待1秒鐘
        TimeUnit.SECONDS.sleep(1);

        new Thread(()->{
            phone.call();
        },"B").start();
    }
}
class Phone{ //資源類
    public synchronized void sendMsm(){
        System.out.println("傳簡訊");
    }
    public synchronized void call(){
        System.out.println("打電話");
    }
}

簡單說明一下:一個資源類裡面有兩個同步方法(傳簡訊 和 打電話),然後主執行緒建立了一個資源類物件,並開了兩個執行緒去分別執行這個物件的兩個同步方法。

執行結果:

傳簡訊
打電話

分析

執行緒A先拿到了 phone 這個物件的鎖,所以他先執行。
這裡 要認為是主執行緒先呼叫了A執行緒所以他先執行。

不理解的話,我們再看一個場景

場景2

import java.util.concurrent.TimeUnit;

/**
 * @ClassName test1
 * @Description
 * @Author SkySong
 * @Date 2020-10-11 18:13
 */
@SuppressWarnings("ALL")
public class test1 {
    public static void main(String[] args) throws InterruptedException {
        Phone phone = new Phone();
        new Thread(()->{
            try {
                phone.sendMsm();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"A").start();
        //等待1秒鐘
        TimeUnit.SECONDS.sleep(1);

        new Thread(()->{
            phone.call();
        },"B").start();
    }
}
class Phone{
    public synchronized void sendMsm() throws InterruptedException {
        //等待3秒鐘
        TimeUnit.SECONDS.sleep(3);
        System.out.println("傳簡訊");
    }
    public synchronized void call(){
        System.out.println("打電話");
    }
}

我們讓 「傳簡訊」方法 持續 3 秒鐘。
執行結果:

傳簡訊
打電話

分析
結果還是先 傳簡訊 後 打電話,就像我 剛剛說的一樣,傳簡訊是持續3秒,並不影響他先得到鎖,因為主執行緒過了一秒才去呼叫執行緒B,所以執行緒A 99.9%的機率是先拿到鎖的。

synchronized 鎖的物件是方法的呼叫者。

執行緒A和B 都是呼叫的同一個物件(phone)的方法,所以他們都是去競爭同一把鎖。誰先拿到鎖誰先執行,沒拿到就得等。


看下一個場景:

如果沒有鎖競爭關係,那麼應該就是拼的速度了。

場景3

import java.util.concurrent.TimeUnit;

/**
 * @ClassName test1
 * @Description
 * @Author SkySong
 * @Date 2020-10-11 18:13
 */
@SuppressWarnings("ALL")
public class test1 {
    public static void main(String[] args) throws InterruptedException {
        Phone phone = new Phone();
        new Thread(()->{
            try {
                phone.sendMsm();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"A").start();
        //等待1秒鐘
        TimeUnit.SECONDS.sleep(1);

        new Thread(()->{
            phone.hello();
        },"B").start();
    }
}
class Phone{
    public synchronized void sendMsm() throws InterruptedException {
        //等待3秒鐘
        TimeUnit.SECONDS.sleep(3);
        System.out.println("傳簡訊");
    }
    public synchronized void call(){
        System.out.println("打電話");
    }
    public void hello(){
        System.out.println("Hello World!");
    }
}

這次我們讓執行緒B呼叫 物件phone的 hello()方法,注意:hello()方法不是同步方法!!

執行結果:

Hello World!
傳簡訊

分析
因為 hello()方法不是同步方法,所以壓根就沒有鎖競爭這一說,雖然 執行緒B 比 執行緒A 晚啟動了一秒鐘,但 「傳簡訊」 要等三秒鐘才列印,這樣一來,Hello World!還會比 「傳簡訊」 先列印 2 秒鐘。

同樣沒有鎖競爭的場景

如下:

場景4

import java.util.concurrent.TimeUnit;

/**
 * @ClassName test1
 * @Description
 * @Author SkySong
 * @Date 2020-10-11 18:13
 */
@SuppressWarnings("ALL")
public class test1 {
    public static void main(String[] args) throws InterruptedException {
        Phone phone = new Phone();
        Phone phone1 = new Phone();
        new Thread(() -> {
            try {
                phone.sendMsm();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "A").start();
        //等待1秒鐘
        TimeUnit.SECONDS.sleep(1);

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

class Phone {
    public synchronized void sendMsm() throws InterruptedException {
        //等待3秒鐘
        TimeUnit.SECONDS.sleep(3);
        System.out.println("傳簡訊");
    }

    public synchronized void call() {
        System.out.println("打電話");
    }
}

這次我們讓 執行緒A 呼叫物件phone的「傳簡訊」方法,而讓 執行緒B 呼叫物件phone1的「打電話」方法。

執行結果:

打電話
傳簡訊

分析
這場景和上一個場景類似,雖然 「打電話」方法也是同步方法,但它是屬於 物件 phone1 的方法,和物件 phone 的 「傳簡訊」 方法 沒有 鎖競爭 關係。

還是那句話:

synchronized 鎖的物件是方法的呼叫者。

顯然,這次的這兩個方法不是同一個呼叫者。而是兩個,物件phone 和 物件phone1。

物件可以鎖,那麼類可不可以鎖?

可以的,看如下場景:

場景5

import java.util.concurrent.TimeUnit;

/**
 * @ClassName test1
 * @Description
 * @Author SkySong
 * @Date 2020-10-11 18:13
 */
@SuppressWarnings("ALL")
public class test1 {
    public static void main(String[] args) throws InterruptedException {
        Phone phone = new Phone();
        Phone phone1 = new Phone();
        new Thread(() -> {
            try {
                phone.sendMsm();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "A").start();
        //等待1秒鐘
        TimeUnit.SECONDS.sleep(1);

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

class Phone {
    public static synchronized void sendMsm() throws InterruptedException {
        //等待3秒鐘
        TimeUnit.SECONDS.sleep(3);
        System.out.println("傳簡訊");
    }

    public static synchronized void call() {
        System.out.println("打電話");
    }
}

基本沒什麼變化,就是把 Phone 資源類 裡的兩個方法都加上了 static ,讓其變成了 靜態方法。

執行結果:

傳簡訊
打電話

分析
被 static 關鍵字修飾的,會在類載入的時候被執行,他們會和類資訊一樣,被存到 方法區,是獨一無二的存在。
所以不管方法的呼叫者是不是同一個物件,他們都會存在 鎖競爭關係,因為他們都是同一個類的靜態方法。所以,這裡的鎖,鎖的是 Class。

判斷 鎖競爭關係的關鍵,就是去看兩個同步方法是不是去拿同一把鎖。

場景6

import java.util.concurrent.TimeUnit;

/**
 * @ClassName test1
 * @Description
 * @Author SkySong
 * @Date 2020-10-11 18:13
 */
@SuppressWarnings("ALL")
public class test1 {
    public static void main(String[] args) throws InterruptedException {
        Phone phone = new Phone();
        new Thread(() -> {
            try {
                phone.sendMsm();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "A").start();
        //等待1秒鐘
        TimeUnit.SECONDS.sleep(1);

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

class Phone {
    public static synchronized void sendMsm() throws InterruptedException {
        //等待3秒鐘
        TimeUnit.SECONDS.sleep(3);
        System.out.println("傳簡訊");
    }

    public static synchronized void call() {
        System.out.println("打電話");
    }
    public static void hello(){
        System.out.println("Hello World!");
    }
}

這次我們又加上了 hello() 方法,我們把它定義成了靜態方法,同樣不是同步方法。

執行結果:

Hello World!
傳簡訊

分析
非同步方法,根本沒有鎖競爭這一說,同一個類(Class)也沒用。
這個場景其實挺沒勁的,都老生常談的事兒了。

那我們在來看下一個場景:

場景7

import java.util.concurrent.TimeUnit;

/**
 * @ClassName test1
 * @Description
 * @Author SkySong
 * @Date 2020-10-11 18:13
 */
@SuppressWarnings("ALL")
public class test1 {
    public static void main(String[] args) throws InterruptedException {
        Phone phone = new Phone();
        new Thread(() -> {
            try {
                phone.sendMsm();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "A").start();
        //等待1秒鐘
        TimeUnit.SECONDS.sleep(1);

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

class Phone {
    public static synchronized void sendMsm() throws InterruptedException {
        //等待3秒鐘
        TimeUnit.SECONDS.sleep(3);
        System.out.println("傳簡訊");
    }

    public synchronized void call() {
        System.out.println("打電話");
    }
}

這次我們把 執行緒B 呼叫的 call()方法 定義成了 普通同步方法,由同一個物件phone 去呼叫這兩個方法。

大家猜猜結果會是神馬?

同一個 物件phone 呼叫的,都是同步方法。結果難道是…

好了,不誤導大家了。

執行結果:

打電話
傳簡訊

分析
還是那句話,看看他們是不是競爭同一把鎖。
首先,「傳簡訊」 是靜態方法,他應該是要去找 Class鎖,而 「打電話」 是非靜態方法,所以他找的是物件鎖。顯然,他們競爭的不是同一把鎖,所以沒有競爭關係。

換句話說,靜態同步塊鎖的是Class,而非靜態的同步塊 鎖的是呼叫者。

That’s all,,Thank you !