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





