| 如果您的程式只是一個單執行緒,單一流程的程式,那麼通常您只要注意到程式邏輯的正確,您的程式通常就可以正確的執行您想要的功能,但當您的程式是多執行緒程式,多流程同時執行時,那麼您就要注意到更多的細節,例如在多執行緒共用同一物件的資料時。 如果一個物件所持有的資料可以被多執行緒同時共享存取時,您必須考慮到「資料同步」的 問題,所謂資料同步指的是兩份資料的整體性一致,例如物件A有 name與id兩個屬性,而有一份A1資料有name與id的資料要更新物件A的屬性,如果A1的name與id設定給A物件完成,則稱A1與A同步,如 果A1資料在更新了物件的name屬性時,突然插入了一份A2資料更新了A物件的id屬性,則顯然的A1資料與A就不同步,A2資料與A也不同步。 資料在多執行緒下共享時,就容易因為同時多個執行緒可能更新同一個物件的資訊,而造成物件資料的不同步,因為資料的不同步而可能引發的錯誤通常不易察覺, 而且可能是在您程式執行了幾千幾萬次之後,才會發生錯誤,而這通常會發生在您的產品已經上線之後,甚至是程式已經執行了幾年之後。 這邊舉個簡單的例子,考慮您設計這麼一個類別:
package onlyfun.caterpillar; 在這個類別中,您可以設定使用者的名稱與縮寫id,並簡單檢查一下名稱與id的第一個字是否相同,單就這個類別本身而言,它並沒有任何的錯誤,但如果它被 用於多執行緒的程式中,而且同一個物件被多個執行存取時,就會"有可能"發生錯誤,來寫個簡單的測試程式:
package onlyfun.caterpillar; 來看一下執行時的一個例子:
看到了嗎?如果以單執行緒的觀點來看,上面的訊息在測試中根本不可能出現,然而在這個程式中卻出現了錯誤,而且重點是,第一次錯誤是發生在第822949 次的設定(您的電腦上可能是不同的數字),如果您在程式完成並開始應用之後,這個時間點可能是幾個月甚至幾年之後。 問題出現哪?在於這邊: public void setNameAndID(String name, String id) {
this.name = name; this.id = id; if(!checkNameAndIDEqual()) { System.out.println(count + ") illegal name or ID....."); } count++; } 雖然您設定給它的參數並沒有問題,在某個時間點時,thread1設定了"Justin Lin", "J.L"給name與id,在進行測試的前一刻,thread2可能此時剛好呼叫setNameAndID("Shang Hwang", "S.H"),在name被設定為"Shang Hwang"時,checkNameAndIDEqual()開始執行,此時name等於"Shang Hwang",而id還是"J.L",所以checkNameAndIDEqual()就會傳回false,結果就顯示了錯誤訊息。 您必須同步資料對物件的更新,也就是在有一個執行緒正在設定person物件的資料時,不可以又被另一個執行緒同時進行設定,您可以使用"synchronized"關鍵字來進行這個動作。 "synchronized"的一個使用方式是用於方法上,讓方法作用範圍內都成為被同步化區域,例如: public synchronized void setNameAndID(String name,
String id) { this.name = name; this.id = id; if(!checkNameAndIDEqual()) { System.out.println(count + ") illegal name or ID....."); } count++; } 每個物件內部都會有一個鎖定(lock),當執行緒執行某個物件的同步化方法時,它會在物件上得到這個鎖定,只有取得鎖定的執行緒才可進入同步區,未取得鎖定的執行緒則必須等待,直到有機會取得鎖定,其它執行緒必須等目前執行緒先執行完同步化方法,並解除對物件的鎖定,才有機會取得物件上的鎖定。 就這個例子來說,簡單的說,就是有執行緒在執行setNameAndID()時,會從物件上取得鎖定,其它執行緒必須等待它執行完畢,釋放鎖定之後,才會有機會競爭鎖定,取得鎖定的執行緒才可以執行setNameAndID ()。 以上所介紹的是實例方法同步化(instance method synchronized),同步化的設定不只可用於方法上,也可以用於某個程式區塊上,稱之為實例區塊同步化(instance block synchronized),例如: public void setNameAndID(String name, String id) {
synchronized(this) { this.name = name; this.id = id; if(!checkNameAndIDEqual()) { System.out.println(count + ") illegal name or ID....."); } count++; } } 上面的意思就是在執行緒執行至"synchronized"設定的區塊時取得物件的鎖定,這麼一來其它執行緒暫時無法取得鎖定,因此無法執行物件同步化區塊,這個方式可以應 用於您不想鎖定整個方法區塊,而只是想在共享資料在被執行緒存取時確保同步化時,由於只鎖定方法中的某個區塊,在執行完區塊後即釋放對物件的鎖定,以便讓 其它執行緒有機會取得鎖定,對物件進行操作,在某些時候會比較有效率。 實例區塊同步化的好處是,您也可以對某個物件進行同步化,而像實例方法同步化只針對this,例如在多執行緒存取同一個ArrayList物件時,ArrayList並沒有實作資料存取時的同步化,所以它使用於多執 行緒時,必須注意是否必須對它進行同步化,多個執行緒存取同一個ArrayList時,有可能發生兩個以上的執行緒將資料存入 ArrayList的同一個位置,造成資料的相互覆蓋,為了確保資料存入時的正確性,您可以在存取ArrayList物件時對它進行同步化,例如: // arraylist參考至一個ArrayList的一個實例
除了針對物件同步之外,您還可以針對靜態方法同步化(static method synchronized),例如某個static成員會被多執行緒存取時,則可以如下設定:synchronized(arraylist) { arraylist.add(new SomeClass()); } public class Some { private static int value; public synchronized static void some() { value++; .... } } 進行鎖定時,會鎖定Some.class,因而static成員也受到保護。類似於實例區塊同步化,您也可以在區塊中鎖定整個類別,稱之為類別字面同步化(class literals synchronized),例如: ... public void doSomething() { synchronized(Some.class) { .... } } ... 事實上,您也可以使用Collections的synchronizedXXX()等方法來傳回一個同步化的容器物件,例如傳回一個同步化的List: List list = Collections.synchronizedList(new ArrayList());
同步化所犧性的自然就是在於執行緒等待時的延遲,所以同步化的手法不應被濫用,您不用將整個物件的方法都加上"synchronized",有些方法只是單純的傳回某些數值,它並沒有對共用資料進行修改的動作,那麼它就不需要被同步化。 |