多線程編程的設計模式 不變模式(二)
不變模式(Immutable Pattern)顧名思義,它的狀態在它的生命週期內是永恒的(暈,永恒的日月星晨,對象如人,
太渺小,談不上永恒!),不會改變的.對於其中的不變類(Immutable Class),它的執行個體可以在運行期間保持狀態永遠不會被
改變,所以不需要採取共用互斥機制來保護,如果運用得當可以節省大量的時間成本.
請注意上面這段話,不變模式其中的不變類,說明不變類只是不變模式中一個組成部分,不變類和與之相輔的可變
類,以及它們之間的關係才共同構成不變模式!所以在涉及不變模式的時候一定要研究一個類是不變的還是可變的(Mutable).
在jdk中的String類和StringBuffer類就組成了一個不變模式.
還是先看具體的例子:
final class Dog{
private final String name;
private final int age;
public Dog(String name,int age){
this.name = name;
this.age = age;
}
public String getName(){return this.name;}
public int getAge(){return this.age;}
public String toString(){
return "Dog's name = " + this.name + ",age = " + this.age;
}
}
1.Dog類本身被聲明為final,可以保證它本身的狀態不會被子類擴充方法所改變.
2.Dog類的所有成員變數都是final的,保證它在構造後不會被重新賦值.而且Dog類所有屬性是private的,只提供getter訪問.
3.Dog類的能傳入的參數本身是Immutable的.這一點非常重要將在下面具體說明.
以上條件都是必要條件,而不是充要條件.
class DisplayDog extends Thread{
private Dog dog;
public DisplayDog(Dog dog){
this.dog = dog;
}
public void run(){
while(true){
System.out.println(this.getName() + " display: " + dog);
}
}
}
DisplayDog類是把一個Dog對象傳入後,不斷顯示這個dog的屬性.我們會同時用多個線程來顯示同一dog對象,看看它們在共用
同一對象時對象的狀態:
public class Test {
public static void main(String[] args) throws Exception {
Dog dog = new Dog("Sager",100);
new DisplayDog(dog).start();
new DisplayDog(dog).start();
new DisplayDog(dog).start();
}
}
運行這個例子你可以等上一個月,雖然運行一年都正常並不能說明第366天不出現異常,但我們可以把這樣的結果認為是一種
說明.多個線程共用一個不變類的執行個體時,這個執行個體的狀態不會發生改變.事實上它沒有地方讓你去改變.
在臨界區模式中有些操作必須只允許有一個線程操作,而這個類本身以及對它的訪問類中並不需要進行臨界區保護,這就讓多
個線程不必等待從而提高了效能.
既然有這麼好的優勢,那我們在需要臨界區保護的對象為什麼不都設計成不變類呢?
1.不變類設計起來有一定難度.對於上面這個用來樣本的Dog,由於其本身的屬性,方法都很簡單,我們還可以充分地考慮到可
以改變它狀態的各種情況.但對於複雜的類,要保證它的不變性,是一個非常吃力的工作.
不變類中,任何一個必要都件都不是充要條件,雖然連老骨灰都沒有這麼說過,但我還是要真誠地目光深邃語氣凝重地告訴你.
沒有任何條件是充要條件的意思就是如果任何一個必要條件你沒考慮到,那它就會無法保證類的不可變性.沒有規範,沒有模
板,完全看一人設計人員的經驗和水平.也許你自以為考慮很全面的一個"不變類"在其他高手面前輕而易舉地就"可變"了.
2.不變類的種種必要條件限制了類設計的全面性,靈活性.這點不用多說,簡單說因為是不變類,所以你不能A,因為是不變類,你
不能B.
當然,如果你是一人很有經驗的設計者,你能成功地設計一個不變類,但因為它的限制而失去一些功能,你就要以使用與之相輔
的可變類.並且它們之間可以相互轉換,在需要不變性操作的時候以不變類提供給使用者,在需要可變性操作的時候以可變類提供
給使用者.
在jdk中String被設計為不可變類,一旦產生一個String對象,它的所有屬性就不會被變,任何方法要麼返回這個對象本身的原
始狀態,要麼拋棄原來的字串返回一個新字串,而絕對不會返回被修改了的字串對象.
但是很多時候返回新字串拋棄原來的字串對象這樣的操作太浪費資源了.特別是在迴圈地操作的時候:
String s = "Axman";
for(int i=0;i<1000*1000;i++) s += "x";這樣的操作是致命的.
那麼這種時候需要將原始的不變的s封裝成可變的StringBuffer來操作,效能的改變可能是成千上萬倍:
StringBuffer sb = new StringBuffer(s); //將不變的String封裝成可變的String;
for(int i=0;i<1000*1000;i++)
sb.append("x");
s = new String(sb); //將可變類封裝成不變類.雖然可以調用toString(),但那不是可變到不變的轉換.
在將可變類封裝到不變類的時候要特別小心.因為你傳入的引用在外面是可以被修改的.所以即使你不變類本身不能去改變屬
性但屬性有一個外部參考.可以在外面修改:
final class MutableDog{
private String name;
private int age;
public MutableDog(String name,int age){
this.name = name;
this.age = age;
}
public synchronized void setDog(String name,int age){
this.name = name;
this.age = age;
}
public String getName(){return this.name;}
public int getAge(){return this.age;}
public synchronized String toString(){
return "Dog's name = " + this.name + ",age = " + this.age;
}
public synchronized ImmatableDog getImmatableDog(){
return new ImmatableDog(this);
}
}
final class ImmatableDog{
private final String name;
private final int age;
public ImmatableDog(String name,int age){
this.name = name;
this.age = age;
}
public ImmatableDog(MutableDog dog){
this.name = dog.getName();
this.age = dog.getAge();
}
public String getName(){return this.name;}
public int getAge(){return this.age;}
public String toString(){
return "Dog's name = " + this.name + ",age = " + this.age;
}
}
MutableDog類是可變的,可以滿足我們利用對象的緩衝來讓對象成為表示另一個實體的功能.但它們之間
隨時可以根據需要相互轉換,但是我們發現:
public ImmatableDog(MutableDog dog){
this.name = dog.getName();
this.age = dog.getAge();
}
這個方法是不安全的.當一個屬性為"Sager",100的dog被傳進來後,執行this.name = dog.getName();後,
如果線程切換到其它線程執行,那麼dog的屬性就可能是"p4",80,這時再執行this.age = dog.getAge();
我們就會得到一個屬性為"Sager",80的這樣一個錯誤的不可變對象.這是一個非常危險的陷井.在這裡我們
可以通過同上來解決:
public ImmatableDog(MutableDog dog){
synchronized(dog){
this.name = dog.getName();
this.age = dog.getAge();
}
}
注意這裡同步的MutableDog,它將會和MutableDog的setDog產生互斥.它們都需要擷取同一MutableDog對象的
鎖,如果MutableDog的setDog不是方法同步(synchronized(this)),即使ImmatableDog(MutableDog dog)中同步
了dog,也不能保證安全,它們需要在同一對象上互斥.
但同步也並不一定能保證傳入的參數不可變:
我曾以下面這個例子來作為對一個Java程式員的終極測試,終極測試的意思是說,如果你不懂並不說明你水平
差,但如何你懂這個問題那就沒有必要測試其它問題了.
public static void test(Object[] objs){
java.security.BasicPermission bp = xxxxx;
for(Object o: objs){
bp.checkGuard(o);
}
for(Object o: abjs){
o.xxx;
}
}
當一個資料被傳入後我們需要對其中的每個元素做安全性檢查,如果通不過bp.checkGuard(o);自己會拋出
異常的.但如果objs[0]被bp.checkGuard(o);過後,外面的線程通過objs去修改objs[0],這時就會把一個沒有
經過安全檢查的對象繞過bp.checkGuard(o);而直接被調用.假如Runtime.exec(String[] args)就是這樣實
現我們可以想象會出現什麼問題.
所以對於這樣的傳入參數,我們可以將其在方法類複製為本地變數(數組).或使用它的深度clone,打斷與方法
外的聯絡:
public static void test(Object[] objs){
Object tmp = new Object[objs.lenth];
System.arrayCopy(objs,tmp,0,0,objs.lenth);
java.security.BasicPermission bp = xxxxx;
for(Object o: tmp){
bp.checkGuard(o);
}
for(Object o: tmp){
o.xxx;
}
}
先說到這裡吧.休息一下.