本文是Common Lisp專家Peter Seibel對Google公司首席Java架構師Joshua Bloch的訪談,談到他所遇到的最糟糕的Bug以及Java的命運。 最糟糕的Bug Seibel:我們聊聊調(diào)試吧。你遇到的最糟糕的Bug是什么? Bloch:提起B(yǎng)ug我立馬就想到了一個,這個Bug很嚴重,而且很搞笑。那是90年代初,我在匹茲堡的 Transarc公司工作時。我在很緊的工期下提交了一個事務共享內(nèi)存的實現(xiàn)。我在限期內(nèi)完成了設計和實現(xiàn),甚至還在過程中做出了幾個可重用的組件。但是這么匆忙地寫了很多新代碼,我還是挺擔心的。 為了測試這些代碼,我寫了一個叫做“亂撞”的很長的程序出來。它運行了大量的事務,每個事務又包含了嵌套的事務,嵌套到可以嵌套的最大深度。每個嵌套事務都可能會加鎖,以遞增的順序讀取共享數(shù)組里面的幾個元素,對每個元素都加入點東西,保持數(shù)組中所有元素的和為0,還是不變量。這些事務要么提交,要么取消,如90%的提交,10%的取消,其他比例也可以。多個線程同步運行于這些事務之上,長時間地訪問數(shù)組。因為我測試的是一個共享內(nèi)存機制,所以我同時運行多個有多個線程的“亂撞”程序,每個有自己的進程。 在一般的并發(fā)級別下,“亂撞”輕松過關。但是當我真正調(diào)高并發(fā)級別時,我發(fā)現(xiàn)“亂撞”偶爾,僅僅是偶爾,無法通過一致性檢查。我不知道這是怎么搞的。這只能是我的錯,因為新代碼都是我一個人寫的。 我花了大約一個星期,痛苦地為每個組件寫了徹底的單元測試,所有的單元測試都通過了。然后我為每個內(nèi)部數(shù)據(jù)結構寫了詳細的一致性檢查,這樣我就可以在每次變化后調(diào)用這些一致性檢查,直到測試失敗為止。最后,我終于發(fā)現(xiàn)一個底層的一致性檢查失敗了,這個問題無法重現(xiàn),但是某種程度上可以幫助我分析問題出在哪里。最后,我得出了確實的結論——我的鎖根本不工作。兩個事務鎖定、讀寫同一個值的時候,產(chǎn)生了并發(fā)的讀—修改—寫回操作,而后一次寫入毀掉了第一次的寫入。 我編寫了自己的鎖管理器,所以我懷疑是它出了問題。但是鎖管理器輕松地通過了測試。最后,我覺得問題不在鎖管理器,而是它依賴的互斥體的實現(xiàn)!那時候操作系統(tǒng)還不支持多線程,我們需要寫自己的多線程包。原來負責互斥體代碼的工程師,不小心把我們的Solaris的線程實現(xiàn)中的lock和try- lock的匯編代碼的標簽弄混了。所以,每次你以為你在調(diào)用lock的時候,其實調(diào)用的是try-lock,反之亦然。也就是說當真的有爭用發(fā)生的時候 ——在當年其實是很罕見的——第二個線程直接就進入了第一個線程的臨界區(qū),因為第一個線程也沒有鎖住。搞笑的是,這也就是說,整個公司幾個星期都在運行沒有互斥體的程序,而且誰都不知道。 Knuth有句關于測試的名言,Bentley和Mcllroy的精彩論文“Engineering a Sort Function”中曾經(jīng)引用過,大概意思是說,做測試時,要不憚以最大的惡意來推測所要測試的代碼的錯誤。做這些測試的時候,我就是這么做的。但是這樣會把所有東西糾結在一起,更難找到Bug。首先,并發(fā)的時候很難這么做,往往完全無法復現(xiàn)場景。其次,到最后可能會發(fā)現(xiàn)你的核心假設是錯的。喜歡喊“耶,這語言出錯了”或者“系統(tǒng)出問題了”是新手干的事兒。但是在這里,我依靠的基石——互斥體,確實出問題了。 Seibel:也就是說Bug不在你的代碼中,但是同時你只能對你的代碼進行徹底的單元測試,因為你沒有別的辦法,只能去檢查自己的代碼。你覺得這些測試是不是可以,或者說應該讓互斥體代碼的作者來寫,這樣你就不用浪費一個星期,節(jié)省了一半的測試量,而且也可以找到這個Bug。 Bloch:給互斥體代碼加一個好的自動化單元測試肯定可以避免讓我遭受那些痛苦,不過注意那可是90年代初。我想都沒想過要抱怨那個工程師沒寫個好的單元測試。即使是今天,為并發(fā)工具寫單元測試還是一種藝術形式。 Java的命運 當你改進一個成熟語言的時候,你必須更加仔細地考慮能力和復雜度之間的平衡。 Seibel:既然你說到這里,Java是否逃脫了滅亡的命運了?它變復雜的速度是不是比變好的速度更快呢? Bloch:這個問題不好回答。具體說來,Java5加入了比我們設想的更多的復雜度。將泛型特別是通配符加到語言中到底有多復雜我也說不好。我得為有功勞的人說句話,GrahamHamilton真是了不起,那時候他就想明白了一切,而我不明白。 有趣的是,他抗爭多年,希望阻止泛型進入Java語言中。但是在泛型被成功地阻擋在Java外的這些年里變體的概念,也就是通配符的隱含意義流行了起來。如果它們來得更早,沒有變體,也許我們現(xiàn)在可以有一個更簡單的、更容易跟蹤的語言。 引入通配符有實際的好處。子類化和泛型之間根本就是阻抗不匹配的,通配符盡力在彌合這種不匹配。但是這么做又顯著地增加了復雜度。有些人認為在聲明空間,而不是用戶空間,變體是更好的解決方案,但我不太相信這一點。 這仍舊懸而未決,因為它們都還沒經(jīng)過在真實世界里海量的程序員們的測試呢。一些語言經(jīng)常只在小范圍內(nèi)獲得成功,人們會說:“噢,這些語言很不錯,只是可惜沒有成為世界范圍成功的語言。”但是這往往是有原因的。希望使用Scala或者C#4.0,這樣的聲明空間變體的語言可以徹底解決這一疑問。 Seibel:那么是什么推動Java引入泛型呢? Bloch:沒看起來那么精彩啦,我們的新聞報道是可信的。我的思維模式是,“嗨,集合多半都應該是同質的 ——一組字符串,一個從字符串到數(shù)字的映射,等等。而現(xiàn)在默認情況下集合是異質的:它們都是對象的集合,取出時都需要類型轉換,這簡直是胡鬧。”如果我可以告訴系統(tǒng),這是一個從字符串到數(shù)字的映射,它會幫我做類型轉換,而且會在編譯期間幫我盯著,防止我做錯什么,那不是挺好的嗎?它可以抓到更多的錯誤—— 它可以包含高層的類型信息,看起來是件好事兒。 我認為泛型和其他加入到Java5的語言特性一樣,我們只是讓語言去做以前我們要手工去做的事情而已。某些情況下我堅信:foreach就是好。它所做的就是對你隱藏遍歷器和索引變量帶來的復雜性。代碼更短,概念也不復雜。從某種意義上說,它的概念更簡單,因為我們?yōu)閿?shù)組和其他的集合創(chuàng)建了這種偽多態(tài)機制,你可以遍歷一個ArrayList或者一個數(shù)組,而無需關心你遍歷的是什么類型。 這種思想不能適用于泛型的主要原因是,它是對已經(jīng)很復雜的類型系統(tǒng)的大擴展。類型系統(tǒng)是很微妙的,修改它們可能對語言帶來深遠的、難以預期的影響。 我認為得到的教訓是,當你改進一個成熟語言的時候,你必須更加仔細地考慮能力和復雜度之間的平衡。而且,實際上,復雜度跟語言的功能數(shù)量間至少是平方級關系。為一門老語言加上了一個新的功能,通常就意味著為它加入了一大堆復雜度。當一種語言已經(jīng)達到或接近程序員理解能力的極限時,那么你加入任何復雜性進來都會加劇理解的難度。 語言更復雜后就會消失嗎?不會。我認為C++早已超越了它的復雜度極限,但還是有很多人用它編程。可這實際上是逼人們只使用其中一個子集。所以我認識的每個用C++的公司都說:“對,我們用C++,但是用的是多繼承,不用操作符重載。”有很多功能你完全不用,因為使用它們會造成代碼太復雜。即使不得不用那些功能,我認為也實在沒什么好處。那樣的話,程序員就讀不懂別人的代碼,也就不存在“程序員的可移植性”了。 Seibel:如果去掉泛型,現(xiàn)在Java會變得更好用嗎? Bloch:我不知道。我還是喜歡泛型。泛型能幫我找到代碼中的Bug。泛型可以讓編譯器強制做一些限制,之前這些限制我只能放在注釋中。另一方面來說,當我看到那些瘋狂的參數(shù)類型相關的錯誤信息,當我看到像 classEnum 我們總是太樂觀,然后搬起石頭砸自己的腳。所以我們說:“耶!我們當然可以把泛型放到Java中。在CLU的時候我們就知道泛型了。這技術25年前就有了。”最近我聽到關于閉包的類似言論,不過那是50年前的技術了。“噢,閉包很簡單,不會給語言加入任何新的復雜性。” 嗯,沒錯。但是我覺得我們從泛型這件事兒得到了教訓。在你懂得這個改動會對概念層面帶來什么影響之前,在你可以確保軟件行業(yè)從業(yè)人員可以高效地使用新特性,而且這一新特性會讓他們活得更好之前,你不應該給語言加入這一特性。 如果早知道程序員們對泛型是這個反應,我們肯定不會把它加到Java里。這是不是說我們就完全不會搞泛型?不,我不這么認為。我認為泛型確實很好。主要是因為大多數(shù)集合是同質的,而不是異質的,同質的集合處理起來是比較方便的。多數(shù)情況下類型轉換都不合適。轉換可能會失敗,而且讓你的程序不再優(yōu)雅。我想你知道這是什么集合,它應該自動符合你的這些需求。但是,是不是這就意味著你應該承受我們現(xiàn)在承受的這種復雜度?不,我想我們只是沒有處理好泛型。 Seibel:關于泛型有來自于用戶的壓力嗎?有人抱怨泛型的缺點干擾他們寫程序了嗎? Bloch:有沒有工程師大罵泛型的缺點?不,沒有,他們沒有抱怨過。如果因為泛型簡潔就把它們加進來,那我會內(nèi)疚的。因為當時我們以為這么做是對的。 有人說,很多工程都是扯淡。有人要求我們加入foreach嗎?沒有。他們沒有要求我加入。但是我就知道這是應該做的。我對了——每個人都喜歡它。但是我覺得我們行業(yè)內(nèi)的一大問題就是,在工程領域,做一個東西,僅僅因為它簡潔,僅僅因為它是一個好的工程項目,等等。如果你不能解決真實用戶——在這里就是Java程序員——的真實問題,那么你就不應該加入新的特性。 James Gosling曾做過一個非常了不起的演講——“Java的感覺”。他說,給Java加入任何東西之前,都需要三個真實的用戶。不應該因為一個東西簡潔就把它放進來。 但是人們就是想把什么東西都放進去。工程師是做什么的?他們就是寫代碼的。而當他們寫一個庫,或者一個語言的時候,他們就是想放各種東西進去。你需要他人的參與,需要指導的聲音,需要這些東西來幫助你完成產(chǎn)品,幫你在放與不放之間做出最好的權衡。因為你可以放進去的東西總比你應該放進去的東西多。那么是不是說所有的這些東西都不好呢?那也不是。只是你需要做出決定,某些東西是不應該放進去的。 思考Java帶來的編程經(jīng)驗 Seibel:思考Java的設計并實現(xiàn)它,是否讓你學到了什么跟編程有關系的東西? Bloch:我學到的東西太多了。比如我知道了即使是想把一個很小的程序寫對也是非常難的。我把這個想法發(fā)表在了博客里,題目是“幾乎所有的二分搜索和歸并排序都是錯的”。認為自己程序是對的就是在愚弄自己。程序里有大量沒解決的Bug,當然是不對的。多數(shù)情況下,程序里的Bug都不少,它們只能免費完成任務。 我知道,既然寫正確的程序那么難,我們就應該盡力去幫助大家。所以能減少Bug的所有東西都是好的。這就是我是靜態(tài)類型和靜態(tài)分析的信徒的原因,任何可以減少某個特定類別Bug的東西都是非常好的,任何可以讓程序員的工作更輕松的東西都是好的。 我更加確信有好的API文檔是很重要的。人們很少提及Javadoc對這個平臺的成功所起的作用。好的API文檔永遠都是Java文化的一部分,也許是因為Javadoc從一開始就存在吧(譯者注:所以人們低估了Javadoc的作用)。 我一直信奉“簡單就是美”這句話,現(xiàn)在更是如此。我不斷看到更復雜的東西最終被證實是有害的,只是有的時間長點兒,有的時間短點兒。我設計的時候,會仔細看著我的“復雜度計”,一旦復雜度要到紅線了,就需要重新設計了。 偶爾我會遇到不相信這些的人們,他們會說:“Josh你太傻了,你怎么就是不明白;這才是應該做的,可惜你就是搞不懂。”我就是不信這些。我覺得事情一旦復雜起來,那么一定有什么地方錯了,也許到了尋找更簡單的方法的時候了。 Tony Hoare的圖靈獎獲獎感言中有一句充滿了大智慧的話,講的是設計一個系統(tǒng)的兩種方式:“一種是盡量簡單,這樣顯然不會有什么問題;另外一種是,盡量復雜,這樣沒什么問題會很顯然。” 后面的內(nèi)容同樣飽含智慧,但是知道的人不多:“第一種方法其實更難。它需要從復雜的自然現(xiàn)象發(fā)現(xiàn)簡單物理規(guī)律的那種技能、投入、洞察力,甚至是那種靈感,同時還需要你能接受你的目標受限于物理、邏輯和科技的約束,以及在目標間有沖突的時候可以妥協(xié)。委員會無法做到這些,除非已經(jīng)完全來不及了。” Seibel:你是否想過在職業(yè)生涯中再次更換你的主要語言,還是準備退休前一直做Java? Bloch:我自己也不知道。我從C語言轉向Java有點突然。從研究生畢業(yè)時起,到1996年,我主要使用 C語言編程,然后一直使用Java直到現(xiàn)在。我已經(jīng)預見到我可能要更換到其他語言了。但是我不知道是什么語言。也許它還不存在。我覺得產(chǎn)生一個新編程語言的時機已經(jīng)成熟,但是同時我又覺得平臺的慣性也比以前更大了。現(xiàn)代的平臺不僅僅是一個語言和一些庫,它包括很多工具,是一個虛擬機,一個龐然大物。創(chuàng)建一個完整的新平臺的前景比以前更不樂觀。 我不知道將出現(xiàn)什么。但是我認為如果改變我的主要語言是對的,那么我就會這么做。我想盡力保持開放的心態(tài)。我想嘗試更多的語言。我最近沒時間做,但是以后還是會做的。 Seibel:列出幾個你想嘗試的語言吧? Bloch:我想試試Scala,雖然我懷疑它是否能成為未來的新寵。我很崇敬MartinOdersky。我覺得他寫的語言中有很多精妙的想法。但是我同時也認為他加入了太多復雜的東西,太學術化了,所以很難取得世界范圍內(nèi)的大成功。當然我還沒權利去評價,因為我還沒學習過。 我還想用用Python。Scheme不是新生物,不過我也想試試。我想花幾個月,跟我兒子一起過一遍《Structure and Interpretation of Computer Programs》一定很有意思。每個人都說這是一本偉大的書。我已經(jīng)買了,算是開了個頭。看完它需要一些時間。我想這就是我目前想學的。 Seibel:現(xiàn)在很多人在討論我們寫程序的時候,如何能把未來的多核CPU的優(yōu)勢利用起來。Java顯然是第一個內(nèi)建多線程機制的主流語言。你覺得Java的邏輯在多核的世界是否仍然可用? Bloch:我想說得更深入一些。我認為Java是現(xiàn)有語言中最好的。但有趣的是,現(xiàn)在很流行談Java是否即將死去。我覺得這基本上是扯淡。我認為現(xiàn)在最好的多線程構件就在Java里。我認為Java將迎來復興。我不是說它是未來20年內(nèi)最先進的,也不是說它是處理多核的最好方式。但是我認為從現(xiàn)有的東西來看,我們是足以傲視同儕的。 |