Java 多執行緒併發程式設計之 Synchronized 關鍵字

hackeris.me發表於2017-04-25

synchronized 關鍵字解析

同步鎖依賴於物件,每個物件都有一個同步鎖。

現有一成員變數 Test,當執行緒 A 呼叫 Test 的 synchronized 方法,執行緒 A 獲得 Test 的同步鎖,同時,執行緒 B 也去呼叫 Test 的 synchronized 方法,此時執行緒 B 無法獲得 Test 的同步鎖,必須等待執行緒 A 釋放 Test 的同步鎖才能獲得從而執行對應方法的程式碼。

綜上,正確使用 synchronized 關鍵字可確保原子性。

synchronized 關鍵字的特性應用

特性 1:

當執行緒 A 呼叫某物件synchronized 方法 或者 synchronized 程式碼塊時,若同步鎖未釋放,其他執行緒呼叫同一物件synchronized 方法 或者 synchronized 程式碼塊時將被阻塞,直至執行緒 A 釋放該物件的同步鎖。

DEMO1,synchronized 方法:

public class Test {

    private static class Counter {

        public synchronized void count() {
            for (int i = 0; i < 6; i++) {
                System.out.println(Thread.currentThread().getName() + ", i = " + i);
            }
        }

    }

    private static class MyThread extends Thread {

        private Counter mCounter;

        public MyThread(Counter counter) {
            mCounter = counter;
        }

        @Override
        public void run() {
            super.run();
            mCounter.count();
        }
    }

    public static void main(String[] var0) {
        Counter counter = new Counter();
        // 注:myThread1 和 myThread2 是呼叫同一個物件 counter
        MyThread myThread1 = new MyThread(counter);
        MyThread myThread2 = new MyThread(counter);
        myThread1.start();
        myThread2.start();
    }

}

DEMO1 輸出:

Thread-0, i = 0
Thread-0, i = 1
Thread-0, i = 2
Thread-0, i = 3
Thread-0, i = 4
Thread-0, i = 5
Thread-1, i = 0
Thread-1, i = 1
Thread-1, i = 2
Thread-1, i = 3
Thread-1, i = 4
Thread-1, i = 5

DEMO2,synchronized 程式碼塊:

public class Test {

    private static class Counter {

        public void count() {
            synchronized (this) {
                for (int i = 0; i < 6; i++) {
                    System.out.println(Thread.currentThread().getName() + ", i = " + i);
                }
            }
        }
    }

    private static class MyThread extends Thread {

        private Counter mCounter;

        public MyThread(Counter counter) {
            mCounter = counter;
        }

        @Override
        public void run() {
            super.run();
            mCounter.count();
        }
    }

    public static void main(String[] var0) {
        Counter counter = new Counter();
        MyThread myThread1 = new MyThread(counter);
        MyThread myThread2 = new MyThread(counter);
        myThread1.start();
        myThread2.start();
    }
}

DEMO2 輸出:

Thread-0, i = 0
Thread-0, i = 1
Thread-0, i = 2
Thread-0, i = 3
Thread-0, i = 4
Thread-0, i = 5
Thread-1, i = 0
Thread-1, i = 1
Thread-1, i = 2
Thread-1, i = 3
Thread-1, i = 4
Thread-1, i = 5

可見,當同步鎖未釋放時,其他執行緒將被阻塞,直至獲得同步鎖。

而且 DEMO1 和 DEMO2 的輸出結果是一樣的,synchronized 方法 和 synchronized 程式碼塊的不同之處在於 synchronized 方法 作用域較大,作用於整個方法,而 synchronized 程式碼塊 可控制具體的作用域,更精準控制提高效率。(畢竟阻塞的都是時間啊)

DEMO3,僅修改 main 方法:

    public static void main(String[] var0) {
        // 注意:myThread1 和 myThread2 傳入的 Counter 是兩個不同的物件
        MyThread myThread1 = new MyThread(new Counter());
        MyThread myThread2 = new MyThread(new Counter());
        myThread1.start();
        myThread2.start();
    }

DEMO3 輸出:

Thread-0, i = 0
Thread-1, i = 0
Thread-0, i = 1
Thread-1, i = 1
Thread-1, i = 2
Thread-1, i = 3
Thread-0, i = 2
Thread-1, i = 4
Thread-0, i = 3
Thread-1, i = 5
Thread-0, i = 4
Thread-0, i = 5

同步鎖基於物件,只要鎖的來源一致,即可達到同步的作用。所以,但物件不一樣,則不能達到同步效果。

特性 2:

當執行緒 A 呼叫某物件synchronized 方法 或者 synchronized 程式碼塊時,若同步鎖未釋放,其他執行緒呼叫同一物件其他synchronized 方法 或者 synchronized 程式碼塊時將被阻塞,直至執行緒 A 釋放該物件的同步鎖。(注意:重點是其他

DEMO4,僅修改 doOtherThings 方法的修飾:

public class Test {

    private static class Counter {

        public synchronized void count() {
            System.out.println(Thread.currentThread().getName() + " sleep");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " awake");
        }

        public synchronized void doOtherThings(){
            System.out.println(Thread.currentThread().getName() + " doOtherThings");
        }
    }

    public static void main(String[] var0) {
        final Counter counter = new Counter();
        new Thread(new Runnable() {
            @Override
            public void run() {
                counter.count();
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                counter.doOtherThings();
            }
        }).start();
    }
}

DEMO4 輸出:

Thread-0 sleep
Thread-0 awake
Thread-1 doOtherThings

可見,synchronized 獲得的同步鎖並非僅僅鎖住程式碼,而是鎖住整個物件。

此時應提及 happens-before 原則,正因 happens-before 原則的存在才有此現象的發生。
happens-before 原則的其中一條:

管理鎖定原則:一個 unLock 操作先行發生於後面對同一個鎖的 lock 操作。
(此處暫不作過多解釋,解釋起來能再寫一篇文章了)

DEMO5,僅修改 doOtherThings 方法:

        public void doOtherThings(){
            synchronized (this){
                System.out.println(Thread.currentThread().getName() + " doOtherThings");
            }
        }

DEMO5 輸出:

Thread-0 sleep
Thread-0 awake
Thread-1 doOtherThings

DEMO4 和 DEMO5 的輸出結果竟然一致!沒錯,因為他們的同步鎖來源一致(都是本例項自己),所以可以達到同步效果。

// 這兩個 synchronized 鎖的是同一個物件
public synchronized void count(){};
public void doOtherThings(){
       synchronized (this){}
}

DEMO6,去掉 doOtherThings 方法的同步關鍵字:

public void doOtherThings(){
            System.out.println(Thread.currentThread().getName() + " doOtherThings");
        }

DEMO6 輸出:

Thread-0 sleep
Thread-1 doOtherThings
Thread-0 awake

當執行緒 A 呼叫某物件synchronized 方法 或者 synchronized 程式碼塊時,無論同步鎖是否釋放,其他執行緒呼叫同一物件其他 非 synchronized 方法 或者 非 synchronized 程式碼塊時可立即呼叫。

例項鎖和全域性鎖

以上 DEMO 實現的都是例項鎖。鎖住(作用域)的是具體某一物件例項。

什麼是全域性鎖?

鎖住整個 Class,而非某個物件或例項。

注:單例型的例項鎖不屬於全域性鎖。

全域性鎖的實現:

靜態 synchronized 方法

DEMO7:

public class Test {

    private static class Counter {

        public static synchronized void count() {
            System.out.println(Thread.currentThread().getName() + " sleep");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " awake");
        }

        public static synchronized void doOtherThings(){
            System.out.println(Thread.currentThread().getName() + " doOtherThings");
        }
    }

    public static void main(String[] var0) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                Counter.count();
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                Counter.doOtherThings();
            }
        }).start();
    }
}

DEMO7 輸出:

Thread-0 sleep
Thread-0 awake
Thread-1 doOtherThings

static 宣告的方法為全域性方法,與物件例項化無關,所以 static synchronized 方法為全域性同步方法,與物件例項化無關。

synchronized 具體 Class 的程式碼塊

DEMO8:

public class Test {

    private static class Counter {

        public static synchronized void count() {
            System.out.println(Thread.currentThread().getName() + " sleep");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " awake");
        }

        public void doOtherThings(){
            synchronized (Counter.class){
                System.out.println(Thread.currentThread().getName() + " doOtherThings");
            }
        }
    }

    public static void main(String[] var0) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                Counter.count();
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                Counter counter = new Counter();
                counter.doOtherThings();
            }
        }).start();
    }
}

DEMO8 輸出:

Thread-0 sleep
Thread-0 awake
Thread-1 doOtherThings

synchronized (Counter.class) 獲得的同步鎖是全域性的,static synchronized 獲得的同步鎖也是全域性的,同一個鎖,所以達到同步效果。

區分 synchronized (this) 與 synchronized (Class.class)

DEMO9:

public class Test {

    private static class Counter {

        public void count() {
            synchronized (this){
                System.out.println(Thread.currentThread().getName() + " sleep");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " awake");
            }
        }

        public void doOtherThings(){
            synchronized (Counter.class){
                System.out.println(Thread.currentThread().getName() + " doOtherThings");
            }
        }
    }

    public static void main(String[] var0) {
        final Counter counter = new Counter();
        new Thread(new Runnable() {
            @Override
            public void run() {
                counter.count();
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                counter.doOtherThings();
            }
        }).start();
    }
}

DEMO9 輸出:

Thread-0 sleep
Thread-1 doOtherThings
Thread-0 awake

synchronized (this) 獲得的是具體物件例項 counter 的鎖,而 synchronized (Counter.class) 獲得的是全域性鎖,兩把不同的鎖,所以不能達到同步效果。

相關文章