多執行緒(二)、內建鎖 synchronized

EvanZch發表於2019-12-12

前言

在上一篇 多執行緒(一)、基礎概念及notify()和wait()的使用 文章中我們講了多執行緒的一些基礎概念還有等待通知機制,在講執行緒之間共享資源的時候,提到會出現資料不同步問題,我們先通過一個示例來演示這個問題。

/**
 * @author : EvanZch
 *         description:
 **/

public class SynchronizedTest {

    // 賦count初始值為0
    public static int count = 0;
    // 進行累加操作
    public void add() {
        count++;
    }

    public static class TestThread extends Thread {
        private SynchronizedTest synchronizedTest;

        public TestThread(SynchronizedTest synchronizedTest) {
            this.synchronizedTest = synchronizedTest;
        }
        @Override
        public void run() {
            super.run();
            // 執行10000次累加
            for (int x = 0; x < 10000; x++) {
                synchronizedTest.add();
            }
        }
    }
    public int getCount() {
        return count;
    }
    public static void main(String[] args) throws InterruptedException {
        SynchronizedTest synchronizedTest = new SynchronizedTest();
        // 開啟兩個執行緒
        new TestThread(synchronizedTest).start();
        new TestThread(synchronizedTest).start();
        int count = synchronizedTest.getCount();
        System.out.println("count=" + count);
    }
}
複製程式碼

可以看到,我程式中我們啟動了兩個執行緒,同時對 Count 變數進行累加操作,每個執行緒迴圈累加10000次,我們預想的結果,獲取的count值應該會是20000,執行程式可以發現。

0?為什麼結果會是0?因為我們在main裡面開啟執行緒執行,方法是順序執行,當執行到 輸出語句的時候,執行緒run方法還沒有啟動,所以這裡列印的是count的初始值 0;

怎麼獲取到正確結果?

1、等待一會在獲取結果

    public static void main(String[] args) throws InterruptedException {
        SynchronizedTest synchronizedTest = new SynchronizedTest();
        new TestThread(synchronizedTest).start();
        new TestThread(synchronizedTest).start();
        // 等待一秒再回去結果
        Thread.sleep(1000);
        int count = synchronizedTest.getCount();
        System.out.println("count=" + count);
    }
複製程式碼

我們在獲取結果之前,先等待一秒,結果如下:

結果不再為 0 ,但是結果也不是我們預想的 20000啊,難道是等待時間不夠?我們增加等待時間,在執行,發現結果也不是20000,這麼看,使用等待時間不嚴謹,因為沒辦法判斷執行緒執行結束時間(其實執行緒執行很快的,遠不需要幾秒),那我們可以使用 join方法。

2、thread.join()

我們先看一下 thread 的 join方法

    /**
     * Waits for this thread to die.
     *
     * <p> An invocation of this method behaves in exactly the same
     * way as the invocation
     *
     * <blockquote>
     * {@linkplain #join(long) join}{@code (0)}
     * </blockquote>
     *
     * @throws  InterruptedException
     *          if any thread has interrupted the current thread. The
     *          <i>interrupted status</i> of the current thread is
     *          cleared when this exception is thrown.
     */

    public final void join() throws InterruptedException {
        join(0);
    }
複製程式碼

註釋大概意思是:當呼叫join方法後,會進行阻塞,直到該執行緒任務執行結束。

可以讓執行緒順序執行。

那我們可以簡單修改程式碼,讓兩個執行緒執行結束後再列印結果

這裡需要注意,我們是在 main 這個執行緒裡面呼叫 join 方法, 則兩個執行緒會在main 執行緒阻塞,但是兩個子執行緒還是在並行處理,都執行結束後才會喚醒 main 執行緒執行後續操作。

    public static void main(String[] args) throws InterruptedException {
        SynchronizedTest synchronizedTest = new SynchronizedTest();
        TestThread testThread = new TestThread(synchronizedTest);
        TestThread testThread1 = new TestThread(synchronizedTest);
        testThread.start();
        testThread1.start();
        // 讓程式順序執行
        testThread.join();
        testThread1.join();
        // 當兩個執行緒任務結束後再獲取結果
        int count = synchronizedTest.getCount();
        System.out.println("count=" + count);
    }
複製程式碼

結果:

發現結果也不是我們預想的 20000,我們使用了 join() 方法,它會在呼叫執行緒進行阻塞(main),當testThreadtestThread1 都執行結束後再喚醒呼叫執行緒 , 能確保兩個執行緒肯定是執行結束了的,可是結果跟預期不一致,多次列印,發現結果一直在 10000 ~ 20000 這個區間波動。

為什麼會出現這種情況?

上一篇文章講過,同一個程式的多個執行緒共享該程式的所有資源,當多個執行緒同時訪問一個物件或者一個物件的成員變數,可能會導致資料不同步問題,比如 執行緒A資料a進行操作,需要從記憶體中進行讀取然後進行相應的操作,操作完成後再寫入記憶體中,但是如果資料還沒有寫入記憶體中的時候,執行緒B 也來對這個資料進行操作,取到的就是還未寫入記憶體的資料,導致前後資料同步問題,我們也叫執行緒不安全操作

比如 執行緒 A 取到 count 的時候,其值為 100,加 1 後再放入記憶體中,如果在放入記憶體之前 執行緒B 也來拿 count 並對其進行累加操作,這個時候 **執行緒B **取到的 count 值 還是100,加 1 後放入記憶體,這個時候值為101, 這樣 執行緒 A 進行累加的那步操作就沒有被算上,這就是為啥,最後兩個執行緒算出來的結果肯定是小於 20000。

怎麼避免這種情況?

我們知道出現這種情況的原因是操作的時候,因為多個執行緒同時訪問一個物件或者物件的成員變數,要處理這個問題,我們就引入了關鍵字 synchronized

正文

一、內建鎖 synchronized

關鍵字 synchronized 可以修飾方法或者以同步塊的形式來進行使用,它主要確保多個執行緒在同一個時刻,只能有一個執行緒處於方法或者同步塊中,它保證了執行緒對變數訪問的可見性排他性,又稱為內建鎖機制

鎖又分為物件鎖和類鎖:

物件鎖: 對一個物件例項進行鎖操作,例項物件可以有多個,不同物件例項的物件鎖互不干擾。

類鎖:用於類的靜態方法或者類的Class物件進行鎖操作。我們知道每個類只有一個Class物件,也就只有一個類鎖。

注意點:

類鎖只是一個概念上的東西,它鎖的也是物件,只不過這個物件是類的Class物件,其唯一存在。

類鎖和物件鎖之間互不干擾。

通過上面的案例,我們簡單改改,我們在執行累加方法上加上 synchronized 關鍵字,然後再執行。

/**
 * @author : EvanZch
 *         description:
 **/

public class SynchronizedTest {

    public static int count = 0;

    // 我們對add方法新增關鍵字 synchronized
    public synchronized void add() {
        count++;
    }

    public static class TestThread extends Thread {
        private SynchronizedTest synchronizedTest;

        public TestThread(SynchronizedTest synchronizedTest) {
            this.synchronizedTest = synchronizedTest;
        }

        @Override
        public void run() {
            super.run();
            for (int x = 0; x < 10000; x++) {
                synchronizedTest.add();
            }
        }
    }

    public int getCount() {
        return count;
    }

    public static void main(String[] args) throws InterruptedException {
        SynchronizedTest synchronizedTest = new SynchronizedTest();
        TestThread testThread = new TestThread(synchronizedTest);
        TestThread testThread1 = new TestThread(synchronizedTest);
        testThread.start();
        testThread1.start();

        // 讓程式順序執行
        testThread.join();
        testThread1.join();

        int count = synchronizedTest.getCount();
        System.out.println("count=" + count);
    }
}
複製程式碼

結果:

可以看到我們只加了一個關鍵字 synchronized ,結果就跟我們預期的 20000 一致,我們將 synchronized

新增到方法上,就確保了多個執行緒同一時刻只有一個執行緒對此方法進行操作,這樣就確保了執行緒安全問題。

前面說了內建鎖存在物件鎖類鎖 ,我們來看一下具體怎麼實現和區別。

1.1、物件鎖

對一個物件例項進行鎖操作,例項物件可以有多個,不同物件例項的物件鎖互不干擾。

我們在前面的示例上進行更改。

方法鎖:

    // 非靜態方法
    public synchronized void add() {
        count++;
    }
複製程式碼

同步程式碼塊鎖:

    public void add(){
        synchronized (this){
            count ++;
        }
    }
複製程式碼

或者:

    // 非靜態變數
    public Object object = new Object();
    public void add(){
        synchronized (object){
            count ++;
        }
    }
複製程式碼

我們可以看到物件鎖都是對非靜態方法和非靜態變數進行加鎖,以上三種從本質上來說沒有區別,我們這個時候再改一下我們的示例程式碼,來驗證一下 不同物件例項的物件鎖互不干擾

    public static void main(String[] args) throws InterruptedException {

        SynchronizedTest synchronizedTest = new SynchronizedTest();
        // 我們再建立一個 SynchronizedTest 物件
        SynchronizedTest synchronizedTest1 = new SynchronizedTest();
        // 傳入 synchronizedTest 
        TestThread testThread = new TestThread(synchronizedTest);
        // 傳入 synchronizedTest1
        TestThread testThread1 = new TestThread(synchronizedTest1);

        testThread.start();
        testThread1.start();
        // 讓程式順序執行
        testThread.join();
        testThread1.join();

        int count = synchronizedTest.getCount();
        System.out.println("count=" + count);
    }
複製程式碼

我們開啟兩個執行緒,分別傳入了不同的例項物件,這個時候再多次執行,檢視執行結果。

結果:

我們多次執行獲取結果,發現都獲取不到我們期望的20000,可以我們明明也在add() 方法上新增了 synchronized 啊,唯一不同的就是,兩個執行緒傳入了不同的物件,所以通過結果,我們可以得出,不同物件的物件鎖之間,是互不影響,各種執行。

1.2、類鎖

用於類的靜態方法或者類的Class物件進行鎖操作。我們知道每個類只有一個Class物件,也就只有一個類鎖。

類鎖其實也是物件鎖,只不過鎖的物件比較特殊。

靜態方法鎖:

    // 靜態方法
    public static synchronized void add() {
        count++;
    }
複製程式碼

同步程式碼塊鎖:

    public void add(){
        // 傳入Class物件
        synchronized (SynchronizedTest.class){
            count ++;
        }
    }
複製程式碼

或者:

    // 靜態成員變數
    public static Object object = new Object();
    public void add(){
        synchronized (object){
            count ++;
        }
    }
複製程式碼

我們知道靜態變數和類的Class物件在記憶體中只存在一個,所以我們對add方法通過類鎖方式進行加鎖,不管外界這個時候傳的物件有多少個,它也是唯一的,我們再執行上面的main方法,列印結果:

可以看到結果和期望一致。

知識擴充 :static 關鍵字和 new 一個物件,做了什麼操作?

static 關鍵字:

  • 靜態變數是隨著類載入時被完成初始化的,它在記憶體中僅有一個,且 JVM 也只會為它分配一次記憶體,同時類所有的例項都共享靜態變數,即一處變、處處變,可以直接通過類名來訪問它。
  • 但是例項變數則不同,它是伴隨著new例項化的,每建立一個例項就會產生一個例項變數,它與該例項同生共死。

new 一個物件,底層做了啥?
1、Jvm載入未載入的位元組碼,開闢空間
2、靜態初始化(1靜態程式碼塊和2靜態變數)
3、成員變數初始化(1普通程式碼塊和2普通成員變數)
4、構造器初始化(建構函式)

相關文章