在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高質量程式碼編寫方面的技巧,請聯絡我們哦。