并發(fā)與線程安全
串行運行時正確的程序在并發(fā)運行時可能會出錯,這是由于并發(fā)運行的多個任務(wù)(進程或線程)之間共享了變量。在Java EE環(huán)境下很多的組件最終都是在多線程環(huán)境下并發(fā)運行的,應(yīng)該牢記住這一點,避免發(fā)生詭異的錯誤。在上個項目中,我們的程序就出現(xiàn)過詭異的錯誤,在并發(fā)量小的測試環(huán)境下,它很少出現(xiàn),但是隨著并發(fā)量的增大,它出現(xiàn)的次數(shù)增多。經(jīng)查,發(fā)現(xiàn)如下類似代碼:
private SomeType someVariable;
public void doPost(HttpServletRequest req, HttpServletRespons rsp) {
method1();
method2();
}
void method1() {
...
someVariable = someValue;
...
}
void method2() {
...
someVariable = someValue;
...
}
}
method1() 和method2()都訪問到了someVariable,而且程序員的本意是想在method2()使用由method1()產(chǎn)生的 someVariable,但是Servlet可能被多個線程共享,上面的代碼不能正常工作。當(dāng)Servlet容器用thread1和thread2兩個線程來服務(wù)兩個請求request1和request2,而thread1和thread2使用了同一個SomeServlet對象,如果JVM在 SomeServlet在執(zhí)行到method1()后和method2()前時發(fā)生線程間的切換,那么就很可能導(dǎo)致出錯,比如:thread1.method1()->thread2.method1()->thread1.method2()->thread2.method2()。實例變量和類變量都是在線程間共享的,而方法內(nèi)的局部變量和參數(shù)是不會被共享的,所以要處理這個問題,最簡單的方法是通過方法參數(shù)來傳遞 someVariable,而不是通過實例變量。所以這段程序改成這樣就可以了:
public void doPost(HttpServletRequest req, HttpServletRespons rsp) {
SomeType someVariable = method1();
method2(someVariable );
}
SomeType someVariablemethod1() {
...
return someValue;
}
void method2(SomeType someVariable) {
...
someVariable = someValue;
...
}
}
通過synchronized等并發(fā)訪問控制機制也可以解決這個問題,但是我覺得上面的方式是最直觀的。也許有人會說后面的代碼比前面的看起來要難看,不夠面向?qū)ο?。這里不討論怎樣才是面向?qū)ο螅蛘呙嫦驅(qū)ο蠛貌缓???傊笳呤钦_的,前者是錯誤的,沒有正確性的程序是沒有意義的。
被過線程共享而不會出錯的對象,被稱之為線程安全的。顯然Servlet不是線程安全的,而且還有很多組件也不是線程安全的,比如HttpSession。在上個項目中,我們使用JAXB來處理XML,每次使用時都創(chuàng)建一個JAXBContext對象,后來發(fā)現(xiàn)創(chuàng)建JAXBContext的代價是相當(dāng)?shù)母?,于是我們想把它緩存起來在整個應(yīng)用程序范圍內(nèi)使用,幸好我們使用的JAXBContext的實現(xiàn)是線程安全的,可以放心的被多個線程共享。
有狀態(tài)與無狀態(tài)
有狀態(tài)組件是這樣一種組件,組件調(diào)用者的返回結(jié)果會依賴于這個組件之前或正在受到的調(diào)用。無狀態(tài)的組件則相反,調(diào)用者的返回結(jié)果只和本次調(diào)用相關(guān),所有調(diào)用者的調(diào)用過程不會影響到其他調(diào)用者。舉例來說,someComponent組件的getLatestCaller()返回上一次的調(diào)用者,這個行為的返回值總會和上一次的調(diào)用者相關(guān),這樣的組件就是有狀態(tài)的。假如有個組件math有個方法為add(x,y),返回x+y的值,那么無論之前被哪個調(diào)用者調(diào)用過,math.add()的返回值只和本次調(diào)用的參數(shù)相關(guān),這樣的組件是無狀態(tài)的。無狀態(tài)的組件是線程安全的,因為被別的調(diào)用者調(diào)用并不會影響到本次的調(diào)用結(jié)果。
除非有必要,否則應(yīng)當(dāng)盡量把你的程序組件實現(xiàn)成無狀態(tài)的。在spring, seam, ejb里都有無狀態(tài)的組件。不同的框架在如何實現(xiàn)無狀態(tài)方面各不相同。最簡單的實現(xiàn)方法就是為無狀態(tài)組件做一個包裝,每次調(diào)用組件的方法時,都由包裝類生成一個新的對象來處理,這樣實際上就不再任何的調(diào)用者之間共享被調(diào)用者的實例,實現(xiàn)了無狀態(tài)。比如:
public SomeType doMyBusinness(SomeArg arg);
}
public class SomeStatlessComponent implements SomeInterface {
public SomeType doMyBusinness(SomeArg arg) {
...
}
}
public class SomeStatlessComponentWrapper implements SomeInterface {
public SomeType doMyBusinness(SomeArg arg) {
return new SomeStatlessComponent().doMyBusinness(arg);
}
}
如果實例化組件的代價是昂貴的,用一個對象池緩存組件實例,并在每次從池中取對象時清空對象的狀態(tài)可能是個更好的辦法。其實只要遵循簡單的原則,就很容易實現(xiàn)無狀態(tài),就是不使用實例變量或類變量,或者只使用只讀的實例變量或只讀類變量。這樣的組件用單例就可以實先無狀態(tài)。
由于實現(xiàn)上的差別,有些框架根本就不會做過多的努力來保證組件的無狀態(tài)性,相反他們把責(zé)任交給了程序員。作為組件的創(chuàng)作者,如果你確定你要的是無狀態(tài)的組件,那么只使用只讀的實例變量或類變量,是個明智的選擇。
相關(guān)文章:





