1. 程式人生 > >深入理解Synchronized關鍵字底層原理及作用(一)

深入理解Synchronized關鍵字底層原理及作用(一)

Synchronized是解決多執行緒問題的常用解決方案,很多程式設計師對Synchronized只是知其然不知其所以然,今天總結一下Synchronized關鍵字的詳細用法及底層實現原理.

當存在多個執行緒操作共享資料時,需要保證同一時刻有且只有一個執行緒在操作共享資料,其他執行緒必須等到該執行緒處理完資料後再進行,這種方式就叫互斥鎖,即能達到互斥訪問目的的鎖,也就是說當一個共享資料被當前正在訪問的執行緒加上互斥鎖後,在同一個時刻,其他執行緒只能處於等待的狀態,直到當前執行緒處理完畢釋放該鎖。在 Java 中,關鍵字 synchronized可以保證在同一個時刻,只有一個執行緒可以執行某個方法或者某個程式碼塊(主要是對方法或者程式碼塊中存在共享資料的操作),同時我們還應該注意到synchronized另外一個重要的作用,synchronized可保證一個執行緒的變化(主要是共享資料的變化)被其他執行緒所看到(保證可見性,完全可以替代Volatile功能),這點確實也是很重要的。

synchronized的三種應用方式

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

  • 修飾例項方法,對當前例項加鎖,進入同步程式碼前要獲得當前例項的鎖

  • 修飾靜態方法,對當前類物件(當前類的Class物件)加鎖,進入同步程式碼前要獲得當前類物件的鎖

  • 修飾程式碼塊,對程式碼塊{}中的內容加鎖,進入同步程式碼塊前要獲得給定物件的鎖。

synchronized作用於例項方法

所謂的例項物件鎖就是用synchronized修飾例項物件中的例項方法,注意是例項方法不包括靜態方法,如下

public class AccountingSync implements Runnable{

    //共享資源(臨界資源)
    static int i=0;

    /**
     * synchronized 修飾例項方法
     */
    public synchronized void increase(){
        i++;
    }

    @Override
    public void run() {
        for(int j=0;j<100000;j++){
            increase();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        AccountingSync instance=new AccountingSync();
        Thread t1=new Thread(instance);
        Thread t2=new Thread(instance);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }

    /**
     * 輸出結果:200000
     * 每個執行緒先讀取i的值,再執行加1操作.
     */
}

這裡兩個執行緒的鎖物件是同一個instance(注意Java中的執行緒同步鎖可以是任意物件),此時當一個執行緒執行instance的synchronized方法時,就持有了該instance的物件鎖,另一個執行緒將無法執行該instance的synchronized方法,因為一個物件只有一把鎖,但可以執行其它非synchronized方法.(變數)

如果兩個執行緒的鎖物件不是同一個instance,由於i++操作並不具備原子性,該操作是先讀取值,再把原來的值加上1寫回,分兩步完成,如果第二個執行緒在第一個執行緒讀取舊值和寫回新值期間讀取i的域值,那麼第二個執行緒就會與第一個執行緒一起看到同一個值,並執行相同值的加1操作,這也就造成了執行緒安全失敗,最後輸出結果將小於200000.

public class AccountingSyncBad implements Runnable{

    static int i=0;

    public synchronized void increase(){
        i++;
    }

    @Override
    public void run() {
        for(int j=0;j<1000000;j++){
            increase();
        }
    }

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

        //new新例項
        Thread t1=new Thread(new AccountingSyncBad());
        //new新例項
        Thread t2=new Thread(new AccountingSyncBad());
        t1.start();
        t2.start();
        //join含義:當前執行緒A等待thread執行緒終止之後才能從thread.join()返回
        t1.join();
        t2.join();
        System.out.println(i);
    }
    /**
     * 輸出結果將小於200000,線上程執行時有可能讀取的i值相同,導致加1後得到的i值相同,
     *最後導致兩個執行緒寫回的i值也相同,注意i是被static修飾的變數.
     */
}

synchronized作用於靜態方法

當synchronized作用於靜態方法時,其鎖就是當前類的class物件鎖。由於靜態成員不專屬於任何一個例項物件,是類成員,因此通過class物件鎖可以控制靜態成員的併發操作。需要注意的是如果一個執行緒A呼叫一個例項物件的非static synchronized方法,而執行緒B需要呼叫這個例項物件所屬類的靜態 synchronized方法,是允許的,不會發生互斥現象,因為訪問靜態 synchronized 方法佔用的鎖是當前類的class物件,而訪問非靜態 synchronized 方法佔用的鎖是當前例項物件鎖,看如下程式碼

public class AccountingSyncClass implements Runnable{

    static int i=0;

    /**
     * 作用於靜態方法,鎖是當前class物件,也就是
     * AccountingSyncClass類對應的class物件
     */
    public static synchronized void increase(){
        i++;
    }

    /**
     * 非靜態,當有其它執行緒訪問時鎖不一樣不會發生互斥
     */
    public synchronized void increase1(){
        i++;
    }

    @Override
    public void run() {
        for(int j=0;j<1000000;j++){
            increase();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        //new新例項
        Thread t1=new Thread(new AccountingSyncClass());
        //new新例項
        Thread t2=new Thread(new AccountingSyncClass());
        //啟動執行緒
        t1.start();t2.start();

        t1.join();t2.join();
        System.out.println(i);
    }
}

由於synchronized關鍵字修飾的是靜態increase方法,與修飾例項方法不同的是,其鎖物件是當前類的class物件。注意程式碼中的increase4Obj方法是例項方法,其物件鎖是當前例項物件,如果別的執行緒呼叫該方法,將不會產生互斥現象,畢竟鎖物件不同,但我們應該意識到這種情況下可能會發現執行緒安全問題(操作了共享靜態變數i)。

synchronized同步程式碼塊

除了使用關鍵字修飾例項方法和靜態方法外,還可以使用同步程式碼塊,在某些情況下,我們編寫的方法體可能比較大,同時存在一些比較耗時的操作,而需要同步的程式碼又只有一小部分,如果直接對整個方法進行同步操作,可能會得不償失,此時我們可以使用同步程式碼塊的方式對需要同步的程式碼進行包裹,這樣就無需對整個方法進行同步操作了,同步程式碼塊的使用示例如下:

public class AccountingSync implements Runnable{

    //靜態例項物件
    static AccountingSync instance=new AccountingSync();

    static int i=0;

    @Override
    public void run() {
        //省略其他耗時操作....
        //使用同步程式碼塊對變數i進行同步操作,鎖物件為instance
        synchronized(instance){
            for(int j=0;j<1000000;j++){
                    i++;
              }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(instance);
        Thread t2=new Thread(instance);
        t1.start();t2.start();
        t1.join();t2.join();
        System.out.println(i);
    }
}

從程式碼看出,將synchronized作用於一個給定的例項物件instance,即當前例項物件就是鎖物件,每次當執行緒進入synchronized包裹的程式碼塊時就會要求當前執行緒持有instance例項物件鎖,如果當前有其他執行緒正持有該物件鎖,那麼新到的執行緒就必須等待,這樣也就保證了每次只有一個執行緒執行i++;操作。當然除了instance作為物件外,我們還可以使用this物件(代表當前例項)或者當前類的class物件作為鎖,如下程式碼:

//this,當前例項物件鎖
synchronized(this){
    for(int j=0;j<1000000;j++){
        i++;
    }
}

//class物件鎖
synchronized(AccountingSync.class){
    for(int j=0;j<1000000;j++){
        i++;
    }
}

瞭解完synchronized的基本含義及其使用方式後,下一篇我們將進一步深入理解synchronized的底層實現原理.

本文參考資料:

《Java程式設計思想》 
《深入理解Java虛擬機器》 
《實戰Java高併發程式設計》