軟件系統(tǒng)的穩(wěn)定性玛荞,主要決定于整體的系統(tǒng)架構(gòu)設(shè)計始腾,然而也不可忽略編程的細(xì)節(jié)州刽,正所謂“千里之堤,潰于蟻穴”浪箭,一旦考慮不周穗椅,看似無關(guān)緊要的代碼片段可能會帶來整體軟件系統(tǒng)的崩潰。這正是我閱讀Release It!的直接感受奶栖。究其原因匹表,一方面是程序員對代碼質(zhì)量的追求不夠,在項目進度的壓力下宣鄙,只考慮了功能實現(xiàn)袍镀,而不用過多的追求質(zhì)量屬性;第二則是對編程語言的正確編碼方式不夠了解冻晤,不知如何有效而正確的編碼苇羡;第三則是知識量的不足,在編程時沒有意識到實現(xiàn)會對哪些因素造成影響明也。
例如在Release It!一書中宣虾,給出了如下的Java代碼片段:
package com.example.cf.flightsearch;
//...
public class FlightSearch implements SessionBean {
private MonitoredDataSource connectionPool;
public List lookupByCity(. . .) throws SQLException, RemoteException {
Connection conn = null;
Statement stmt = null;
try {
conn = connectionPool.getConnection();
stmt = conn.createStatement();
// Do the lookup logic
// return a list of results
} finally {
if (stmt != null) {
stmt.close();
}
if (conn != null) {
conn.close();
}
}
}
}
正是這一小段代碼惯裕,是造成Airline系統(tǒng)崩潰的罪魁禍?zhǔn)住3绦騿T充分地考慮了資源的釋放绣硝,但在這段代碼中他卻沒有對多個資源的釋放給予足夠的重視蜻势,而是以釋放單資源的做法去處理多資源。在finally語句塊中鹉胖,如果釋放Statement資源的操作失敗了握玛,就可能拋出異常,因為在finally中并沒有捕獲這種異常甫菠,就會導(dǎo)致后面的conn.close()語句沒有執(zhí)行挠铲,從而導(dǎo)致Connection資源未能及時釋放。最終導(dǎo)致連接池中存放了大量未能及時釋放的Connection資源寂诱,卻不能得到使用拂苹,直到連接池滿。當(dāng)后續(xù)請求lookupByCity()時痰洒,就會在調(diào)用connectionPool.getConnection()方法時被阻塞瓢棒。這些被阻塞的請求會越來越多,最后導(dǎo)致資源耗盡丘喻,整個系統(tǒng)崩潰脯宿。
Release It!的作者對Java中同步方法的使用也提出了警告。同步方法雖然可以較好地解決并發(fā)問題泉粉,在一定程度上可以避免出現(xiàn)資源搶占连霉、竟態(tài)條件和死鎖的情況。但它的一個副作用同步鎖可能導(dǎo)致線程阻塞嗡靡。這就要求同步方法的執(zhí)行時間不能太長跺撼。此外,Java的接口方法是不能標(biāo)記synchronized關(guān)鍵字叽躯。當(dāng)我們在調(diào)用封裝好的第三方API時财边,基于“面向接口設(shè)計”的原理肌括,可能調(diào)用者只知道公開的接口方法点骑,卻不知道實現(xiàn)類事實上將其實現(xiàn)為同步方法,這種未知性就可能存在隱患谍夭。
假設(shè)有這樣的一個接口:
public interface GlobalObjectCache {
public Object get(String id);
}
如果接口方法get()的實現(xiàn)如下:
public synchronized Object get(String id){
Object obj = items.get(id);
if(obj == null) {
obj = create(id);
items.put(id, obj);
}
return obj;
}
protected Object create(String id) {
//...
}
這段代碼很簡單黑滴,當(dāng)調(diào)用者試圖根據(jù)id獲得目標(biāo)對象時,首先會在Cache中尋找紧索,如果有就直接返回袁辈;否則通過create()方法獲得目標(biāo)對象,然后再將它存儲到Cache中珠漂。create()方法是該類定義的一個非final方法晚缩,它執(zhí)行了DB的查詢功能∥膊玻現(xiàn)在,假設(shè)使用該類的用戶對它進行了擴展荞彼,例如定義RemoteAvailabilityCache類派生該類冈敛,并重寫create()方法,將原來的本地調(diào)用改為遠(yuǎn)程調(diào)用鸣皂。問題出現(xiàn)了抓谴。由于采用create()方法是遠(yuǎn)程調(diào)用,當(dāng)服務(wù)端比較繁忙時寞缝,發(fā)出的遠(yuǎn)程調(diào)用請求可能會被阻塞癌压。由于get()方法是同步方法,在方法體內(nèi)荆陆,每次只能有一個線程訪問它滩届,直到方法執(zhí)行完畢釋放鎖。現(xiàn)在create()方法被阻塞被啼,就會導(dǎo)致其他試圖調(diào)用RemoteAvailabilityCache對象的get()方法的線程隨之而被阻塞丐吓。進而可能導(dǎo)致系統(tǒng)崩潰。
當(dāng)然趟据,我們可以認(rèn)為這種擴展本身是不合理的券犁。但從設(shè)計的角度來看,它并沒有違背Liskove替換原則汹碱。從接口的角度看粘衬,它的行為也沒有發(fā)生任何改變,僅僅是實現(xiàn)發(fā)生了變化咳促。如果不是同步方法稚新,則一個調(diào)用線程的阻塞并不會影響到其他調(diào)用線程,問題就可以避免了跪腹。當(dāng)然褂删,這里的同步方法本身是合理的,因為只有采取同步的方式才能保證對Cache的讀取是支持并發(fā)的冲茸。書中給出這個例子屯阀,無非是要說明同步方法潛在的危險,提示我們在編寫代碼時轴术,需要考慮周全难衰。