軟體系統的穩定性,主要決定於整體的系統架構設計,然而也不可忽略編程的細節,正所謂“千裡之堤,潰於蟻穴”,一旦考慮不周,看似無關緊要的程式碼片段可能會帶來整體軟體系統的崩潰。這正是我閱讀Release It!的直接感受。究其原因,一方面是程式員對代碼品質的追求不夠,在項目進度的壓力下,只考慮了功能實現,而不用過多的追求品質屬性;第二則是對程式設計語言的正確編碼方式不夠瞭解,不知如何有效而正確的編碼;第三則是知識量的不足,在編程時沒有意識到實現會對哪些因素造成影響。
例如在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系統崩潰的罪魁禍首。程式員充分地考慮了資源的釋放,但在這段代碼中他卻沒有對多個資源的釋放給予足夠的重視,而是以釋放單資源的做法去處理多資源。在finally語句塊中,如果釋放Statement資源的操作失敗了,就可能拋出異常,因為在finally中並沒有捕獲這種異常,就會導致後面的conn.close()語句沒有執行,從而導致Connection資源未能及時釋放。最終導致串連池中存放了大量未能及時釋放的Connection資源,卻不能得到使用,直到串連池滿。當後續請求lookupByCity()時,就會在調用connectionPool.getConnection()方法時被阻塞。這些被阻塞的請求會越來越多,最後導致資源耗盡,整個系統崩潰。
Release It!的作者對Java中同步方法的使用也提出了警告。同步方法雖然可以較好地解決並發問題,在一定程度上可以避免出現資源搶佔、竟態條件和死結的情況。但它的一個副作用同步鎖可能導致線程阻塞。這就要求同步方法的執行時間不能太長。此外,Java的介面方法是不能標記synchronized關鍵字。當我們在調用封裝好的第三方API時,基於“面向介面設計”的原理,可能調用者只知道公開的介面方法,卻不知道實作類別事實上將其實現為同步方法,這種未知性就可能存在隱患。
假設有這樣的一個介面:
public interface GlobalObjectCache {public Object get(String id);}
如果介面方法get()的實現如下:
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) {//...}
這段代碼很簡單,當調用者試圖根據id獲得目標對象時,首先會在Cache中尋找,如果有就直接返回;否則通過create()方法獲得目標對象,然後再將它儲存到Cache中。create()方法是該類定義的一個非final方法,它執行了DB的查詢功能。現在,假設使用該類的使用者對它進行了擴充,例如定義RemoteAvailabilityCache類派生該類,並重寫create()方法,將原來的本地調用改為遠程調用。問題出現了。由於採用create()方法是遠程調用,當服務端比較繁忙時,發出的遠程調用請求可能會被阻塞。由於get()方法是同步方法,在方法體內,每次只能有一個線程訪問它,直到方法執行完畢釋放鎖。現在create()方法被阻塞,就會導致其他試圖調用RemoteAvailabilityCache對象的get()方法的線程隨之而被阻塞。進而可能導致系統崩潰。
當然,我們可以認為這種擴充本身是不合理的。但從設計的角度來看,它並沒有違背Liskove替換原則。從介面的角度看,它的行為也沒有發生任何改變,僅僅是實現發生了變化。如果不是同步方法,則一個調用線程的阻塞並不會影響到其他調用線程,問題就可以避免了。當然,這裡的同步方法本身是合理的,因為只有採取同步的方式才能保證對Cache的讀取是支援並發的。書中給出這個例子,無非是要說明同步方法潛在的危險,提示我們在編寫代碼時,需要考慮周全。