線程間的同步與通訊(1)——同步代碼塊Synchronized

前言

同步代碼塊(Synchronized Block) 是java中最基礎的實現線程間的同步與通訊的機制之一,本篇咱們將對同步代碼塊以及監視器鎖的概念進行討論。java

系列文章目錄編程

什麼是同步代碼塊(Synchronized Block)

同步代碼塊簡單來講就是將一段代碼用一把給鎖起來, 只有得到了這把鎖的線程才訪問, 而且同一時刻, 只有一個線程能持有這把鎖, 這樣就保證了同一時刻只有一個線程能執行被鎖住的代碼.segmentfault

這裏有兩個關鍵字須要注意: 一段代碼.併發

一段代碼

通常來講, 由 synchronized 鎖住的代碼都是拿{}括起來的代碼塊:函數

synchronized(this) {
    //由鎖保護的代碼
}

但值得注意的是, synchronized 也能夠用來修飾一個方法, 則對應的被鎖保護的一段代碼很天然就是整個方法體.this

public class Foo {
    public synchronized void doSomething() {
        // 由鎖保護的代碼
    }
}

其實鎖這個東西提及來很抽象, 你能夠就把它想象成現實中的鎖, 因此它只不過是一塊令牌, 一把尚方寶劍, 它是木頭作的仍是金屬作的並不重要, 你能夠拿任何東西看成鎖, 重要的是它表明的含義: 誰持有它, 誰就有獨立訪問臨界區(即上面所說的一段代碼)的權利..net

在java中, 咱們能夠拿一個對象看成鎖.線程

這裏引用<<java併發編程實戰>>中的一段話:code

每一個java對象均可以用作一個實現同步的鎖, 這些鎖被稱爲內置鎖(Intrinsic Lock)或者監視器鎖(Monitor Lock). 線程在進入同步代碼塊以前會自動得到鎖, 而且在退出同步代碼塊時自動釋放鎖.

得到內置鎖的惟一途徑就是進入由這個鎖保護的同步代碼塊或方法.對象

因此, synchronized 同步代碼塊的標準寫法應該是:

synchronized(reference-to-lock) {
    //臨界區
}

其中, 括號裏面的reference-to-lock就是鎖的引用, 它只要指向一個Java對象就行, 你能夠本身隨便new一個不相關的對象, 將它做爲鎖放進去, 也能夠像以前的例子同樣, 直接使用this, 表明使用當前對象做爲鎖.

有的同窗就要問了, 咱們前面說能夠用synchronized修飾一個方法, 而且也知道對應的由鎖保護的代碼塊就是整個方法體, 可是, 它的鎖是什麼呢?
要回答這個問題,首先要區分synchronized 所修飾的方法是不是靜態方法:

若是 synchronized所修飾的是靜態方法, 則其所用的鎖爲 Class對象
若是 synchronized所修飾的是非靜態方法, 則其所用的鎖爲 方法調用所在的對象

當使用synchronized 修飾非靜態方法時, 如下兩種寫法是等價的:

//寫法1
public synchronized void doSomething() {
    // 由鎖保護的代碼
}

//寫法2
public void doSomething() {
    synchronized(this) {
        // 由鎖保護的代碼
    }
}

到底拿什麼鎖住了同步代碼塊

同步代碼塊中最難理解的部分就是拿什麼做爲了鎖, 上面咱們已經提到了三個 this, Class對象, 方法調用所在的對象, 而且咱們也說明了能夠拿任何java對象做爲鎖.

this方法調用所在的對象

這兩個實際上是一個意思, 咱們須要特別注意的是, 一個Class能夠有多個實例(Instance), 每個Instance均可以做爲鎖, 不一樣Instance就是不一樣的鎖, 同一個Instance就是同一個鎖, this方法調用所在的對象 指代的都是調用這個同步代碼塊的對象.

這麼說可能比較抽象, 咱們直接上例子: (如下例子轉載自博客Java中Synchronized的用法)

class SyncThread implements Runnable {
    private static int count;

    public SyncThread() {
        count = 0;
    }

    public  void run() {
        synchronized(this) {
            for (int i = 0; i < 5; i++) {
                try {
                    System.out.println(Thread.currentThread().getName() + ":" + (count++));
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    public static void main(String[] args) {
        SyncThread syncThread = new SyncThread();
        //線程1和線程2使用了SyncThread類的同一個對象實例
        //所以, 這兩個線程中的synchronized(this), 持有的是同一把鎖
        Thread thread1 = new Thread(syncThread, "SyncThread1");
        Thread thread2 = new Thread(syncThread, "SyncThread2");
        thread1.start();
        thread2.start();
    }
}

運行結果:

SyncThread1:0
SyncThread1:1
SyncThread1:2
SyncThread1:3
SyncThread1:4
SyncThread2:5
SyncThread2:6
SyncThread2:7
SyncThread2:8
SyncThread2:9

這裏兩個線程SyncThread1SyncThread2 持有同一個對象syncThread的鎖, 所以同一時刻, 只有一個線程能訪問同步代碼塊, 線程SyncThread2 只有等 SyncThread1 執行完同步代碼塊後, SyncThread1線程自動釋放了鎖, 隨後 SyncThread2才能獲取同一把鎖, 進入同步代碼塊.

咱們也能夠修改一下main函數, 讓兩個線程持有同一Class對象的不一樣實例的鎖:

public static void main(String[] args) {
    Thread thread1 = new Thread(new SyncThread(), "SyncThread1");
    Thread thread2 = new Thread(new SyncThread(), "SyncThread2");
    thread1.start();
    thread2.start();
}

上面這段等價於:

public static void main(String[] args) {
    SyncThread syncThread1 = new SyncThread();
    SyncThread syncThread2 = new SyncThread();
    Thread thread1 = new Thread(syncThread1, "SyncThread1");
    Thread thread2 = new Thread(syncThread2, "SyncThread2");
    thread1.start();
    thread2.start();
}

運行結果:

SyncThread1:0
SyncThread2:1
SyncThread1:2
SyncThread2:3
SyncThread1:4
SyncThread2:5
SyncThread1:6
SyncThread2:7
SyncThread1:8
SyncThread2:9

可見, 兩個線程此次都能訪問同步代碼塊, 這是由於線程1執行的是syncThread1對象的同步代碼塊, 線程2執行的是syncThread2的同步代碼塊, 雖然這兩個同步代碼塊同樣, 可是他們在不一樣的對象實例裏面, 即雖然它們都用this做爲鎖, 可是this指代的對象在這兩個線程中不是同一個對象, 兩個線程各自都能得到鎖, 所以各自都能執行這一段同步代碼塊.

這告訴咱們, 當一段代碼用同步代碼塊包起來的時候, 並不絕對意味着這段代碼同一時刻只能由一個線程訪問, 這種狀況只發生在多個線程訪問的是同一個Instance, 也就是說, 多個線程請求的是同一把鎖.

再回顧咱們上面兩個例子, 第一個例子中, 兩個線程使用的是同一個對象實例, 他們須要同一把對象鎖 syncThread,
第二個例子中, 兩個線程分別使用了一個對象實例, 他們分別請求的是本身訪問的對象實例的鎖syncThread1, syncThread2, 所以都能訪問同步代碼塊.

致使不一樣線程能夠同時訪問同步代碼塊的最根本緣由就是咱們使用的是當前實例對象鎖(this), 由於類的實例能夠有多個, 這致使了同步代碼塊散佈在類的多個實例中, 雖然同一個實例中的同步代碼塊只能由持有鎖的單個線程訪問(this對象鎖保護), 可是咱們能夠每一個線程訪問本身的對象實例, 而每個對象實例的同步代碼塊都是一致的, 這就間接致使了多個線程同時訪問了"同一個"同步代碼塊.

上面這種狀況在某些條件下是沒有問題的, 例如同步代碼塊中不存在對靜態變量(共享的狀態量)的修改.

可是, 對於上面的例子, 這樣的狀況明顯違背了咱們加同步代碼塊的初衷.

要解決上面的狀況, 一種可行的辦法就是像第一個例子同樣, 多個線程使用同一個對象實例, 例如在單例模式下, 自己就只有一個對象實例, 因此多個線程必將請求同一把鎖, 從而實現同步訪問.

另外一種方法就是咱們下面要講的: 使用Class鎖.

使用Class級別鎖

前面咱們提到:

若是 synchronized所修飾的是靜態方法, 則其所用的鎖爲 Class對象

這是由於靜態方法是屬於類的而不屬於對象的, 所以synchronized修飾的靜態方法鎖定的是這個類的全部對象。咱們來看下面一個例子(如下例子一樣轉載自博客Java中Synchronized的用法):

class SyncThread implements Runnable {
    private static int count;

    public SyncThread() {
        count = 0;
    }

    // synchronized 關鍵字加在一個靜態方法上
    public synchronized static void staticMethod() {
        for (int i = 0; i < 5; i ++) {
            try {
                System.out.println(Thread.currentThread().getName() + ":" + (count++));
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void run() {
        staticMethod();
    }

    public static void main(String[] args) {
        SyncThread syncThread1 = new SyncThread();
        SyncThread syncThread2 = new SyncThread();
        Thread thread1 = new Thread(syncThread1, "SyncThread1");
        Thread thread2 = new Thread(syncThread2, "SyncThread2");
        thread1.start();
        thread2.start();
    }
}

運行結果:

SyncThread1:0
SyncThread1:1
SyncThread1:2
SyncThread1:3
SyncThread1:4
SyncThread2:5
SyncThread2:6
SyncThread2:7
SyncThread2:8
SyncThread2:9

可見, 靜態方法鎖定了類的全部對象, 用咱們以前的話來講, 若是說"由於類的實例能夠有多個, 這致使了同步代碼塊散佈在類的多個實例中", 那麼類的靜態方法就是阻止同步代碼塊散佈在類的實例中, 由於類的靜態方法只屬於類自己.

其實, 上面的例子的本質就是拿Class對象做爲鎖, 咱們前面也提到了, 能夠拿任何對象做爲鎖, 若是咱們直接拿類的Class對象做爲鎖, 一樣能夠保證因此線程請求的都是同一把鎖, 由於Class對象只有一個.

類鎖其實是經過對象鎖實現的,即類的 Class 對象鎖。每一個類只有一個 Class 對象,因此每一個類只有一個類鎖。
class SyncThread implements Runnable {
    private static int count;

    public SyncThread() {
        count = 0;
    }

    public void run() {
        // 這裏直接拿Class對象做爲鎖
        synchronized(SyncThread.class) {
            for (int i = 0; i < 5; i++) {
                try {
                    System.out.println(Thread.currentThread().getName() + ":" + (count++));
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) {
        SyncThread syncThread1 = new SyncThread();
        SyncThread syncThread2 = new SyncThread();
        Thread thread1 = new Thread(syncThread1, "SyncThread1");
        Thread thread2 = new Thread(syncThread2, "SyncThread2");
        thread1.start();
        thread2.start();
    }
}

這樣所獲得的結果與上面的類的靜態方法加鎖是一致的。

幾點補充

其實到這裏, 重要的部分已經講完了, 下面補充說明幾點:

(1) 當一個線程訪問對象的一個synchronized(this)同步代碼塊時,另外一個線程仍然能夠訪問該對象中的非synchronized(this)同步代碼塊。

這個結論是顯而易見的, 在沒有加鎖的狀況下, 全部的線程均可以自由地訪問對象中的代碼, 而synchronized關鍵字只是限制了線程對於已經加鎖的同步代碼塊的訪問, 並不會對其餘代碼作限制.
這裏也提示咱們:

同步代碼塊應該越短小越好

(2) 當一個線程訪問object的一個synchronized(this)同步代碼塊時,其餘線程對object中全部其它synchronized(this)同步代碼塊的訪問將被阻塞。

這個結論也是顯而易見的, 由於synchronized(this)拿的都是當前對象的鎖, 若是一個線程已經進入了一個同步代碼塊, 說明它已經拿到了鎖, 而訪問同一個object中的其餘同步代碼塊一樣須要當前對象的鎖, 因此它們會被阻塞.

(3) synchronized關鍵字不能繼承。

對於父類中用synchronized 修飾的方法,子類在覆蓋該方法時,默認狀況下不是同步的,必須顯式的使用 synchronized 關鍵字修飾才行, 固然子類也能夠直接調用父類的方法, 這樣就間接實現了同步.

(4) 在定義接口方法時不能使用synchronized關鍵字。
(5) 構造方法不能使用synchronized關鍵字,但可使用synchronized代碼塊來進行同步。
(6) 離開同步代碼塊後,所得到的鎖會被自動釋放。

總結

  • synchronized關鍵字經過一把鎖住一段代碼, 使得線程只有在持有鎖的時候才能訪問這段代碼
  • 任何java對象均可以做爲這把鎖
  • 能夠在synchronized後面用()顯式的指定鎖. 也能夠直接做用在方法上

    • 做用於普通方法時, 至關於以this對象做爲鎖, 此時同步代碼塊散佈於類的全部實例中, 每個實例的同步代碼塊的鎖 爲該實例對象自身。
    • 做用於靜態方法時, 至關於以Class對象做爲鎖, 此時對象的全部實例只能爭搶同一把鎖。
  • 內置鎖的一個重要的特性是當離開同步代碼塊以後, 會自動釋放鎖,而其餘的高級鎖(如ReentrantLock)須要顯式釋放鎖。

思考題

前面咱們說明了synchronized 的使用方法,但對一些底層的細節並不瞭解,如:

  1. 前面說「得到內置鎖的惟一途徑就是進入由這個鎖保護的同步代碼塊或方法.」, 這句話看上去頗有道理,實際上是廢話,同步代碼塊到底是怎麼得到鎖的?
  2. 咱們說,JAVA中任何對象均可以做爲鎖,那麼鎖信息是怎麼被記錄和存儲的?
  3. 爲何代碼離開了同步代碼塊鎖就被釋放了,誰釋放了鎖,怎樣叫釋放了鎖?

這些問題,咱們後續的文章再研究。

(完)

查看更多系列文章:系列文章目錄

相關文章
相關標籤/搜索