標籤:字元 image 代碼品質 exception 可見 read abc 子類 編寫
在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,就相當於讓非靜態方法具備執行緒安全性,這帶來的一個問題是,如果應用程式中該類型存在多個執行個體,在遇到這個鎖的時候,都會產生同步,而這可能不是我們原先所願意看到的。
第三點:實值型別(基礎資料型別 (Elementary Data Type))對象不能作為同步對象
實際上,這樣的代碼也不會通過編譯。
實值型別在傳遞另一個線程的時候,會建立一個副本,這相當於每個線程鎖定的也是兩個對象。故,實值型別對象不能作為同步對象。這一點實際也可以歸結到第一點中。
第四點,鎖定字串是完全沒有必要,而且相當危險的
這整個過程看上去和實值型別正好相反。字串在虛擬機器中會被暫存到記憶體裡,如果有兩個變數被分配了相同內容的字串,那麼這兩個引用會被指向同一塊記憶體。所以,如果有兩個地方同時使用了synchronized (“abc”),那麼它們實際鎖定的是同一個對象,導致整個應用程式被阻滯。
第五點:降低同步對象的可見度
同步對象一般來說,不應該是一個public變數,我們應該始終考慮降低同步對象的可見度,將我們的同步對象藏起來,只開放給自己或自己的子類就夠了(需要開放給子類的情況其實也不多見)。
以下是廣告時間:最課程(zuikc.com)正在招收Java就業班學員,如果你想學習更多的Java高品質代碼編寫方面的技巧,請聯絡我們哦。
Java代碼品質改進之:同步對象的選擇