Java程式碼質量改進之:同步物件的選擇

陸敏技發表於2018-07-27

  在Java中,讓執行緒同步的一種方式是使用synchronized關鍵字,它可以被用來修飾一段程式碼塊,如下:

     synchronized(被鎖的同步物件) {
          // 程式碼塊:業務程式碼
     }

  當synchronized被用來修飾程式碼塊的時候表示,如果有多個執行緒正在執行這段程式碼塊,那麼需要等到其中一個執行緒執行完畢,第二個執行緒才會再執行它。但是!如果被鎖的同步物件沒有被正確選擇的話,上面的結論是不正確的哦。

到底什麼樣的物件能夠成為一個鎖物件(也叫同步物件)?我們在選擇同步物件的時候,應當始終注意以下幾點:

第一點,需要鎖定的物件在多個執行緒中是可見的、同一個物件

  “可見的”這是顯而易見的,如果物件不可見,就不能被鎖定。“同一個物件”,這理解起來也很好理解,如果鎖定的不是同一個物件,那又如何來同步兩個物件呢?可是,不見得我們在這上面不會犯錯誤。為了闡述本建議,我們先模擬一個必須使用到鎖的場景:火車站賣火車票。一列火車一共有100張票,一共有3個視窗在同時賣票,程式碼如下:

package com.zuikc.thread;

public class SynchronizedSample01 {
    public static void main(String[] args) {
        // 建立
        TicketWindow window1 = new TicketWindow("售票視窗1");
        TicketWindow window2 = new TicketWindow("售票視窗2");
        TicketWindow window3 = new TicketWindow("售票視窗3");

        window1.start();
        window2.start();
        window3.start();
    }

}

class TicketWindow extends Thread {
    // 共100個座位
    static int ticket = 100;

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

    @Override
    public void run() {
        // 模擬賣票
        while (ticket > 0) {

            System.out.println(this.getName() + "賣出了座位號:" + ticket);
            ticket--;

            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

  可是,執行之後,我們發現我們的火車票有些座位被賣了多次,比如:

  只要多執行幾次,我們就會看到不同的結果。但是幾乎每次都會有被座位號被賣多次的現象發生。

  有同學可能會說,簡單:加synchronized鎖定同步物件,於是我們修改程式碼:

class TicketWindow extends Thread {
    // 共100個座位
    static int ticket = 100;

    // 定義被鎖的同步物件
    Object obj = new Object();

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

    @Override
    public void run() {
        // 想要同步的程式碼塊
        synchronized (obj) {
            // 模擬賣票
            while (ticket > 0) {

                System.out.println(this.getName() + "賣出了座位號:" + ticket);
                ticket--;

                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

       執行之後,我們發現結果沒有任何的改變。為什麼吶?

       因為在3個執行緒中,我們鎖定的不是同一個物件。

       我們看到,被鎖的是一個例項變數,如下:

       Object obj = new Object();

       而存在三個執行緒,就意味著生成了3個obj,每個執行緒鎖定的是這3個不同的obj物件,所以,同步程式碼塊等於沒有被同步。

       那應該怎麼做呢?最簡單的方法是,我們可以把例項變數改成成員變數,即靜態變數,如下:

       Static Object obj = new Object();

       然後,再執行售票程式碼,就發現可以解決這個問題了。不信,試試看。

第二個注意事項:非靜態方法中,靜態變數不應作為同步物件

  上面剛說完,要修正第一點中的示例,需要將obj變成static。這似乎和本注意事項有矛盾。實際上,第一點中的示例程式碼僅出於演示的目的,在編寫多執行緒程式碼時,我們可以遵循這樣的一個原則:型別的靜態方法應當保證執行緒安全,非靜態方法不需實現執行緒安全。而如果將syncObject變成static,就相當於讓非靜態方法具備執行緒安全性,這帶來的一個問題是,如果應用程式中該型別存在多個例項,在遇到這個鎖的時候,都會產生同步,而這可能不是我們原先所願意看到的。

第三點:值型別(基本資料型別)物件不能作為同步物件

  實際上,這樣的程式碼也不會通過編譯。

值型別在傳遞另一個執行緒的時候,會建立一個副本,這相當於每個執行緒鎖定的也是兩個物件。故,值型別物件不能作為同步物件。這一點實際也可以歸結到第一點中。

第四點,鎖定字串是完全沒有必要,而且相當危險的

  這整個過程看上去和值型別正好相反。字串在虛擬機器中會被暫存到記憶體裡,如果有兩個變數被分配了相同內容的字串,那麼這兩個引用會被指向同一塊記憶體。所以,如果有兩個地方同時使用了synchronized (“abc”),那麼它們實際鎖定的是同一個物件,導致整個應用程式被阻滯。

第五點:降低同步物件的可見性

  同步物件一般來說,不應該是一個public變數,我們應該始終考慮降低同步物件的可見性,將我們的同步物件藏起來,只開放給自己或自己的子類就夠了(需要開放給子類的情況其實也不多見)。

以下是廣告時間:最課程(zuikc.com)正在招收Java就業班學員,如果你想學習更多的Java高質量程式碼編寫方面的技巧,請聯絡我們哦。

 

相關文章