天天看點

面試系列-高并發之synchronized

本章,會對synchronized關鍵字進行介紹。涉及到的内容包括:

synchronized原理

synchronized基本規則

synchronized方法 和 synchronized代碼塊

執行個體鎖 和 全局鎖

在java中,每一個對象有且僅有一個同步鎖。這也意味着,同步鎖是依賴于對象而存在。

當我們調用某對象的synchronized方法時,就擷取了該對象的同步鎖。例如,synchronized(obj)就擷取了“obj這個對象”的同步鎖。

不同線程對同步鎖的通路是互斥的。也就是說,某時間點,對象的同步鎖隻能被一個線程擷取到!通過同步鎖,我們就能在多線程中,實作對“對象/方法”的互斥通路。 例如,現在有兩個線程A和線程B,它們都會通路“對象obj的同步鎖”。假設,在某一時刻,線程A擷取到“obj的同步鎖”并在執行一些操作;而此時,線程B也企圖擷取“obj的同步鎖” —— 線程B會擷取失敗,它必須等待,直到線程A釋放了“該對象的同步鎖”之後線程B才能擷取到“obj的同步鎖”進而才可以運作。

我們将synchronized的基本規則總結為下面3條,并通過執行個體對它們進行說明。

第一條:

當一個線程通路“某對象”的“synchronized方法”或者“synchronized代碼塊”時,其他線程對“該對象”的該“synchronized方法”或者“synchronized代碼塊”的通路将被阻塞。

第二條:

當一個線程通路“某對象”的“synchronized方法”或者“synchronized代碼塊”時,其他線程仍然可以通路“該對象”的非同步代碼塊。

第三條:

當一個線程通路“某對象”的“synchronized方法”或者“synchronized代碼塊”時,其他線程對“該對象”的其他的“synchronized方法”或者“synchronized代碼塊”的通路将被阻塞。

第一條:

下面是“synchronized代碼塊”對應的示範程式。

class MyRunable implements Runnable {

    @Override
    public void run() {
        synchronized(this) {
            try {  
                for (int i = 0; i < 5; i++) {
                    Thread.sleep(100); // 休眠100ms
                    System.out.println(Thread.currentThread().getName() + " loop " + i);  
                }
            } catch (InterruptedException ie) {  
            }
        }  
    }
}

public class Demo1_1 {

    public static void main(String[] args) {  
        Runnable demo = new MyRunable();     // 建立“Runnable對象”

        Thread t1 = new Thread(demo, "t1");  // 建立“線程t1”, t1是基于demo這個Runnable對象
        Thread t2 = new Thread(demo, "t2");  // 建立“線程t2”, t2是基于demo這個Runnable對象
        t1.start();                          // 啟動“線程t1”
        t2.start();                          // 啟動“線程t2” 
    } 
}           

運作結果:

t1 loop 0
t1 loop 1
t1 loop 2
t1 loop 3
t1 loop 4
t2 loop 0
t2 loop 1
t2 loop 2
t2 loop 3
t2 loop 4           

結果說明:

run()方法中存在“synchronized(this)代碼塊”,而且t1和t2都是基于"demo這個Runnable對象"建立的線程。這就意味着,我們可以将synchronized(this)中的this看作是“demo這個Runnable對象”;是以,線程t1和t2共享“demo對象的同步鎖”。是以,當一個線程運作的時候,另外一個線程必須等待“運作線程”釋放“demo的同步鎖”之後才能運作。

如果你确認,你搞清楚這個問題了。那我們将上面的代碼進行修改,然後再運作看看結果怎麼樣,看看你是否會迷糊。修改後的源碼如下:

class MyThread extends Thread {

    public MyThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        synchronized(this) {
            try {  
                for (int i = 0; i < 5; i++) {
                    Thread.sleep(100); // 休眠100ms
                    System.out.println(Thread.currentThread().getName() + " loop " + i);  
                }
            } catch (InterruptedException ie) {  
            }
        }  
    }
}

public class Demo1_2 {

    public static void main(String[] args) {  
        Thread t1 = new MyThread("t1");  // 建立“線程t1”
        Thread t2 = new MyThread("t2");  // 建立“線程t2”
        t1.start();                          // 啟動“線程t1”
        t2.start();                          // 啟動“線程t2” 
    } 
}           

代碼說明:

比較Demo1_2 和 Demo1_1,我們發現,Demo1_2中的MyThread類是直接繼承于Thread,而且t1和t2都是MyThread子線程。

幸運的是,在“Demo1_2的run()方法”也調用了synchronized(this),正如“Demo1_1的run()方法”也調用了synchronized(this)一樣!

那麼,Demo1_2的執行流程是不是和Demo1_1一樣呢?

t1 loop 0
t2 loop 0
t1 loop 1
t2 loop 1
t1 loop 2
t2 loop 2
t1 loop 3
t2 loop 3
t1 loop 4
t2 loop 4           

如果這個結果一點也不令你感到驚訝,那麼我相信你對synchronized和this的認識已經比較深刻了。否則的話,請繼續閱讀這裡的分析。

synchronized(this)中的this是指“目前的類對象”,即synchronized(this)所在的類對應的目前對象。它的作用是擷取“目前對象的同步鎖”。

對于Demo1_2中,synchronized(this)中的this代表的是MyThread對象,而t1和t2是兩個不同的MyThread對象,是以t1和t2在執行synchronized(this)時,擷取的是不同對象的同步鎖。對于Demo1_1對而言,synchronized(this)中的this代表的是MyRunable對象;t1和t2共同一個MyRunable對象,是以,一個線程擷取了對象的同步鎖,會造成另外一個線程等待。

第二條:

class Count {

    // 含有synchronized同步塊的方法
    public void synMethod() {
        synchronized(this) {
            try {  
                for (int i = 0; i < 5; i++) {
                    Thread.sleep(100); // 休眠100ms
                    System.out.println(Thread.currentThread().getName() + " synMethod loop " + i);  
                }
            } catch (InterruptedException ie) {  
            }
        }  
    }

    // 非同步的方法
    public void nonSynMethod() {
        try {  
            for (int i = 0; i < 5; i++) {
                Thread.sleep(100);
                System.out.println(Thread.currentThread().getName() + " nonSynMethod loop " + i);  
            }
        } catch (InterruptedException ie) {  
        }
    }
}

public class Demo2 {

    public static void main(String[] args) {  
        final Count count = new Count();
        // 建立t1, t1會調用“count對象”的synMethod()方法
        Thread t1 = new Thread(
                new Runnable() {
                    @Override
                    public void run() {
                        count.synMethod();
                    }
                }, "t1");

        // 建立t2, t2會調用“count對象”的nonSynMethod()方法
        Thread t2 = new Thread(
                new Runnable() {
                    @Override
                    public void run() {
                        count.nonSynMethod();
                    }
                }, "t2");  


        t1.start();  // 啟動t1
        t2.start();  // 啟動t2
    } 
}           
t1 synMethod loop 0
t2 nonSynMethod loop 0
t1 synMethod loop 1
t2 nonSynMethod loop 1
t1 synMethod loop 2
t2 nonSynMethod loop 2
t1 synMethod loop 3
t2 nonSynMethod loop 3
t1 synMethod loop 4
t2 nonSynMethod loop 4           

主線程中建立了兩個子線程t1和t2。t1會調用count對象的synMethod()方法,該方法内含有同步塊;而t2則會調用count對象的nonSynMethod()方法,該方法不是同步方法。t1運作時,雖然調用synchronized(this)擷取“count的同步鎖”;但是并沒有造成t2的阻塞,因為t2沒有用到“count”同步鎖。

第三條:

我們将上面的例子中的nonSynMethod()方法體的也用synchronized(this)修飾。修改後的源碼如下:

class Count {

    // 含有synchronized同步塊的方法
    public void synMethod() {
        synchronized(this) {
            try {  
                for (int i = 0; i < 5; i++) {
                    Thread.sleep(100); // 休眠100ms
                    System.out.println(Thread.currentThread().getName() + " synMethod loop " + i);  
                }
            } catch (InterruptedException ie) {  
            }
        }  
    }

    // 也包含synchronized同步塊的方法
    public void nonSynMethod() {
        synchronized(this) {
            try {  
                for (int i = 0; i < 5; i++) {
                    Thread.sleep(100);
                    System.out.println(Thread.currentThread().getName() + " nonSynMethod loop " + i);  
                }
            } catch (InterruptedException ie) {  
            }
        }
    }
}

public class Demo3 {

    public static void main(String[] args) {  
        final Count count = new Count();
        // 建立t1, t1會調用“count對象”的synMethod()方法
        Thread t1 = new Thread(
                new Runnable() {
                    @Override
                    public void run() {
                        count.synMethod();
                    }
                }, "t1");

        // 建立t2, t2會調用“count對象”的nonSynMethod()方法
        Thread t2 = new Thread(
                new Runnable() {
                    @Override
                    public void run() {
                        count.nonSynMethod();
                    }
                }, "t2");  


        t1.start();  // 啟動t1
        t2.start();  // 啟動t2
    } 
}           
t1 synMethod loop 0
t1 synMethod loop 1
t1 synMethod loop 2
t1 synMethod loop 3
t1 synMethod loop 4
t2 nonSynMethod loop 0
t2 nonSynMethod loop 1
t2 nonSynMethod loop 2
t2 nonSynMethod loop 3
t2 nonSynMethod loop 4
           

主線程中建立了兩個子線程t1和t2。t1和t2運作時都調用synchronized(this),這個this是Count對象(count),而t1和t2共用count。是以,在t1運作時,t2會被阻塞,等待t1運作釋放“count對象的同步鎖”,t2才能運作。

synchronized方法和synchronized代碼塊

"synchronized方法"是用synchronized修飾方法,而"synchronized代碼塊"則是用synchronized修飾代碼塊。

synchronized方法示例

public synchronized void foo1() {
    System.out.println("synchronized methoed");
}
synchronized代碼塊
public void foo2() {
    synchronized (this) {
        System.out.println("synchronized methoed");
    }
}           

synchronized代碼塊中的this是指目前對象。也可以将this替換成其他對象,例如将this替換成obj,則foo2()在執行synchronized(obj)時就擷取的是obj的同步鎖。

synchronized代碼塊可以更精确的控制沖突限制通路區域,有時候表現更高效率。下面通過一個示例來示範:

// Demo4.java的源碼
public class Demo4 {

    public synchronized void synMethod() {
        for(int i=0; i<1000000; i++)
            ;
    }

    public void synBlock() {
        synchronized( this ) {
            for(int i=0; i<1000000; i++)
                ;
        }
    }

    public static void main(String[] args) {
        Demo4 demo = new Demo4();

        long start, diff;
        start = System.currentTimeMillis();                // 擷取目前時間(millis)
        demo.synMethod();                                // 調用“synchronized方法”
        diff = System.currentTimeMillis() - start;        // 擷取“時間內插補點”
        System.out.println("synMethod() : "+ diff);

        start = System.currentTimeMillis();                // 擷取目前時間(millis)
        demo.synBlock();                                // 調用“synchronized方法塊”
        diff = System.currentTimeMillis() - start;        // 擷取“時間內插補點”
        System.out.println("synBlock()  : "+ diff);
    }
}           

(某一次)執行結果:

synMethod() : 11
synBlock() : 3           

原文釋出時間為:2018-07-05

本文作者:skywang12345

本文來自雲栖社群合作夥伴“

Java架構師之路

”,了解相關資訊可以關注“

”。

社群技術交流:歡迎關注【

阿裡Java技術進階

】每周在群内進行【免費技術直播】和【線上回答技術問題】歡迎點選link入群:

http://tb.cn/gXRstIw

或者 釘釘掃碼入群:

面試系列-高并發之synchronized

繼續閱讀