1. 程式人生 > >多執行緒(2):synchronized關鍵字

多執行緒(2):synchronized關鍵字

  多執行緒操作相同資源的時候,會出現執行緒安全的問題,導致結果與預期的不一致。

  如下例子,設計四個執行緒,其中兩個對執行緒對變數加1操作,兩個執行緒對變數減1操作。理想狀態是,執行緒順序執行,相同次數的加減操作,最後變數的值不變。

1.執行緒不安全的操作

public class FourThreadAddMinus {

    private int i = 0;

    public static void main(String[] args) {
        FourThreadAddMinus ft = new FourThreadAddMinus();
        for(int i = 0; i < 2; i++){
            new Thread(ft.new Add(), "add執行緒-" + i).start();
            new Thread(ft.new Minus(),"minus執行緒-" + i).start();
        }
    }

    // 內部類,加1操作(不用內部類也行,這裡偷懶,把程式碼都放到一個class檔案下)
    class Add implements Runnable{

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

    // 內部類,減1操作
    class Minus implements Runnable{

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

    private void add(){
        i++;
        System.out.println(System.currentTimeMillis() + " - " + Thread.currentThread().getName() + " 執行 >>> " + i);

    }

    private void minus(){
        i--;
        System.out.println(System.currentTimeMillis() + " - " + Thread.currentThread().getName() + " 執行 >>> " + i);

    }
}

  列印輸出結果:

1542793183193 - add執行緒-0 執行 >>> 1
1542793183193 - minus執行緒-1 執行 >>> 0
1542793183193 - add執行緒-0 執行 >>> 1
1542793183193 - minus執行緒-1 執行 >>> 0
1542793183193 - add執行緒-0 執行 >>> 1
1542793183193 - minus執行緒-0 執行 >>> -1
1542793183193 - add執行緒-0 執行 >>> 0
1542793183193 - minus執行緒-1 執行 >>> 0
1542793183194 - add執行緒-0 執行 >>> 0
1542793183194 - minus執行緒-0 執行 >>> -1
1542793183194 - minus執行緒-1 執行 >>> -1
1542793183194 - minus執行緒-0 執行 >>> -2
1542793183194 - minus執行緒-1 執行 >>> -3
1542793183194 - add執行緒-1 執行 >>> -3
1542793183195 - add執行緒-1 執行 >>> -2
1542793183195 - add執行緒-1 執行 >>> -1
1542793183195 - add執行緒-1 執行 >>> 0
1542793183194 - minus執行緒-0 執行 >>> -4
1542793183195 - minus執行緒-0 執行 >>> 0
1542793183195 - add執行緒-1 執行 >>> 1

  從執行的結果看,同一時刻,有多個執行緒同時執行且順序不定,會出現某個執行緒拿到的值是另一個執行緒未儲存的,也可以理解成資料庫裡面的髒讀,造成的結果就是共享資源變數i的值不可控,每次執行,結果可能都不一樣。這不是我們想要的結果。

  這種情況,我們需要控制執行緒的併發狀態,即同一時刻,只能有一個執行緒對變數i進行操作,其他執行緒阻塞。等該執行緒執行結束,i的值進行變更後,其他執行緒拿到最新的i值再進行操作。

2.執行緒安全的操作(多執行緒搶佔同一例項的鎖)

  java中提供了一個關鍵字,synchronized可以控制執行緒的併發。

  對程式碼進行如果修改add()和minuss()方法加上,synchronized關鍵字

public class FourThreadAddMinus {

    private int i = 0;

    public static void main(String[] args) {
        FourThreadAddMinus ft = new FourThreadAddMinus();
        for(int i = 0; i < 2; i++){
            new Thread(ft.new Add(), "add執行緒-" + i).start();
            new Thread(ft.new Minus(),"minus執行緒-" + i).start();
        }
    }

    // 內部類,加1操作(不用內部類也行,這裡偷懶,把程式碼都放到一個class檔案下)
    class Add implements Runnable{

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

    // 內部類,減1操作
    class Minus implements Runnable{

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

    private synchronized void add(){
        i++;
        System.out.println(System.currentTimeMillis() + " - " + Thread.currentThread().getName() + " 執行 >>> " + i);

    }

    private synchronized void minus(){
        i--;
        System.out.println(System.currentTimeMillis() + " - " + Thread.currentThread().getName() + " 執行 >>> " + i);

    }
}

  列印輸出結果:  

1542793702921 - add執行緒-0 執行 >>> 1
1542793702922 - add執行緒-0 執行 >>> 2
1542793702922 - add執行緒-0 執行 >>> 3
1542793702922 - add執行緒-0 執行 >>> 4
1542793702922 - add執行緒-0 執行 >>> 5
1542793702923 - minus執行緒-0 執行 >>> 4
1542793702923 - minus執行緒-0 執行 >>> 3
1542793702924 - minus執行緒-0 執行 >>> 2
1542793702924 - minus執行緒-0 執行 >>> 1
1542793702924 - add執行緒-1 執行 >>> 2
1542793702924 - add執行緒-1 執行 >>> 3
1542793702924 - add執行緒-1 執行 >>> 4
1542793702924 - add執行緒-1 執行 >>> 5
1542793702924 - add執行緒-1 執行 >>> 6
1542793702924 - minus執行緒-0 執行 >>> 5
1542793702925 - minus執行緒-1 執行 >>> 4
1542793702925 - minus執行緒-1 執行 >>> 3
1542793702925 - minus執行緒-1 執行 >>> 2
1542793702925 - minus執行緒-1 執行 >>> 1
1542793702926 - minus執行緒-1 執行 >>> 0

  從執行的結果看,由於add()和minus()方法被synchronized修飾,執行緒執行該方法,需要獲取當前例項的鎖而當前例項只有一個FourThreadAddMinus ft = new FourThreadAddMinus()所以,假設add執行緒-0獲取鎖以後,minus執行緒-0,minus執行緒-1,add執行緒-1,則處於阻塞狀態,。待add執行緒-0執行完,鎖釋放後,其他執行緒搶佔資源,獲取鎖。此時執行緒one by one執行,最後i的值變為初識狀態0。

3.執行緒不安全的操作(多執行緒搶佔多例項的鎖)

  程式碼再進一步修改,new兩個FourThreadAddMinus例項。

public class FourThreadAddMinus {

    private int i = 0;

    public static void main(String[] args) {
        FourThreadAddMinus ft1 = new FourThreadAddMinus();
        FourThreadAddMinus ft2 = new FourThreadAddMinus();
        for(int i = 0; i < 2; i++){
            new Thread(ft1.new Add(), "add執行緒-" + i).start();
            new Thread(ft2.new Minus(),"minus執行緒-" + i).start();
        }
    }

    // 內部類,加1操作(不用內部類也行,這裡偷懶,把程式碼都放到一個class檔案下)
    class Add implements Runnable{

        @Override
        public void run() {
            for(int i = 0; i < 5; i++) {
                add();
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    // 內部類,減1操作
    class Minus implements Runnable{

        @Override
        public void run() {
            for(int i = 0; i < 5; i++){
                minus();
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private synchronized void add(){
        i++;
        System.out.println(System.currentTimeMillis() + " - " + Thread.currentThread().getName() + " 執行 >>> " + i);

    }

    private synchronized void minus(){
        i--;
        System.out.println(System.currentTimeMillis() + " - " + Thread.currentThread().getName() + " 執行 >>> " + i);

    }
}

  列印輸出結果:

1542795420885 - add執行緒-0 執行 >>> 1
1542795420886 - minus執行緒-0 執行 >>> -1
1542795420886 - add執行緒-1 執行 >>> 2
1542795420886 - minus執行緒-1 執行 >>> -2
1542795421386 - minus執行緒-1 執行 >>> -3
1542795421386 - add執行緒-1 執行 >>> 3
1542795421386 - minus執行緒-0 執行 >>> -4
1542795421386 - add執行緒-0 執行 >>> 4
1542795421887 - add執行緒-1 執行 >>> 5
1542795421887 - add執行緒-0 執行 >>> 6
1542795421887 - minus執行緒-0 執行 >>> -5
1542795421887 - minus執行緒-1 執行 >>> -6
1542795422388 - minus執行緒-1 執行 >>> -7
1542795422388 - add執行緒-0 執行 >>> 7
1542795422388 - minus執行緒-0 執行 >>> -8
1542795422388 - add執行緒-1 執行 >>> 8
1542795422888 - add執行緒-0 執行 >>> 9
1542795422888 - minus執行緒-1 執行 >>> -9
1542795422888 - add執行緒-1 執行 >>> 10
1542795422888 - minus執行緒-0 執行 >>> -10

  從結果看,add兩個執行緒順序執行,將i從0加到10;minus兩個執行緒順序執行,將i從0減到-10。但這裡,add執行緒操作的i和minus操作的i不是同一個,因為是兩個例項。鎖也只是保證了兩個add執行緒(或兩個minus的執行緒)的順序。其實我是想寫,四個執行緒,搶兩個例項鎖,操作同一個資源。不刪了,就當複習一下多執行緒(1)的內容。程式碼再改一下就好了,將 private int i = 0,改成 private static int i = 0。加一個static關鍵字,變成所有例項共享的變數。完整程式碼就不放了,跟上面的一樣,就是多個static關鍵字。

  列印輸出結果:

1542796917053 - add執行緒-0 執行 >>> 1
1542796917054 - minus執行緒-0 執行 >>> 0
1542796917054 - add執行緒-1 執行 >>> 1
1542796917054 - minus執行緒-1 執行 >>> 0
1542796917554 - add執行緒-0 執行 >>> 0
1542796917554 - minus執行緒-1 執行 >>> 0
1542796917554 - add執行緒-1 執行 >>> 1
1542796917554 - minus執行緒-0 執行 >>> 0
1542796918054 - minus執行緒-0 執行 >>> 1
1542796918054 - add執行緒-0 執行 >>> 1
1542796918054 - minus執行緒-1 執行 >>> 0
1542796918054 - add執行緒-1 執行 >>> 1
1542796918555 - add執行緒-1 執行 >>> 1
1542796918555 - minus執行緒-1 執行 >>> 1
1542796918555 - minus執行緒-0 執行 >>> 1
1542796918555 - add執行緒-0 執行 >>> 2
1542796919056 - add執行緒-1 執行 >>> 2
1542796919056 - minus執行緒-1 執行 >>> 2
1542796919056 - add執行緒-0 執行 >>> 3
1542796919056 - minus執行緒-0 執行 >>> 2

  從結果看,由於add執行緒和minus執行緒,搶佔的不是同一個例項的鎖,所以add和minus之前互不影響,同一時刻,可能add和minus都在執行,而且由於操作同一個資源i,就會出現髒讀的情況,最後結果也不可控。

4.synchronized關鍵字的使用:

  如果一個方法被synchronized修飾了,當一個執行緒獲取了對應的鎖,並執行該程式碼塊時,其他執行緒便只能一直等待,等待獲取鎖的執行緒釋放鎖,而這裡獲取鎖的執行緒釋放鎖只會有兩種情況:

  1. 獲取鎖的執行緒執行完了該程式碼塊,然後執行緒釋放對鎖的佔有;
  2. 執行緒執行發生異常,此時JVM會讓執行緒自動釋放鎖。

  synchronized可保證一個執行緒的變化(主要是共享資料的變化)被其他執行緒所看到(保證可見性,完全可以替代Volatile功能),這點確實也是很重要的。

  synchronized關鍵字最主要有以下3種應用方式

  1. 修飾例項方法,作用於當前例項加鎖,進入同步程式碼前要獲得當前例項的鎖(上面講的都是這種用法)
  2.  修飾靜態方法,作用於當前類物件加鎖,進入同步程式碼前要獲得當前類物件的鎖
  3.  修飾程式碼塊,指定加鎖物件,對給定物件加鎖,進入同步程式碼庫前要獲得給定物件的鎖。

  修飾靜態方法,也好說,把程式碼改一下。 

    private synchronized static void add(){
        i++;
        System.out.println(System.currentTimeMillis() + " - " + Thread.currentThread().getName() + " 執行 >>> " + i);

    }

    private synchronized static void minus(){
        i--;
        System.out.println(System.currentTimeMillis() + " - " + Thread.currentThread().getName() + " 執行 >>> " + i);

    }

  列印輸出結果: 

1542797628413 - add執行緒-0 執行 >>> 1
1542797628413 - minus執行緒-0 執行 >>> 0
1542797628413 - minus執行緒-1 執行 >>> -1
1542797628413 - add執行緒-1 執行 >>> 0
1542797628914 - minus執行緒-1 執行 >>> -1
1542797628914 - minus執行緒-0 執行 >>> -2
1542797628914 - add執行緒-0 執行 >>> -1
1542797628914 - add執行緒-1 執行 >>> 0
1542797629414 - add執行緒-0 執行 >>> 1
1542797629414 - add執行緒-1 執行 >>> 2
1542797629414 - minus執行緒-1 執行 >>> 1
1542797629414 - minus執行緒-0 執行 >>> 0
1542797629915 - add執行緒-0 執行 >>> 1
1542797629915 - add執行緒-1 執行 >>> 2
1542797629915 - minus執行緒-1 執行 >>> 1
1542797629915 - minus執行緒-0 執行 >>> 0
1542797630415 - add執行緒-1 執行 >>> 1
1542797630415 - add執行緒-0 執行 >>> 2
1542797630415 - minus執行緒-0 執行 >>> 1
1542797630415 - minus執行緒-1 執行 >>> 0

   從輸出結果看,執行緒之間還是 one by one執行,因為add()和minus()方法都是靜態方法,為當前類物件所有,不屬於某一個例項。不管建立多少例項,類的物件都是那一個。多執行緒搶佔的是類物件的鎖,只有一把鎖,所以執行緒之間互斥,誰搶到鎖,誰執行。