Java 8無人談及的八大功能

infoq發表於2014-07-04

 時間戳鎖

  一直以來,多執行緒程式碼是伺服器開發人員的毒藥(問問Oracle的Java語言架構師和並行開發大師Brian Goetz)。Java的核心庫不斷加入各種複雜的用法來減少訪問共享資源時的執行緒等待時間。其中之一就是經典的讀寫鎖(ReadWriteLock),它讓你把程式碼分成兩部分:需要互斥的寫操作和不需要互斥的讀操作。

  表面上看起來很不錯。問題是讀寫鎖有可能是極慢的(最多10倍),這已經和它的初衷相悖了。Java 8引入了一種新的讀寫鎖——叫做時間戳鎖。好訊息是這個傢伙真的非常快。壞訊息是它使用起來更復雜,有更多的狀態需要處理。並且它是不可重入的,這意味著一個執行緒有可能跟自己死鎖。

  時間戳鎖有一種“樂觀”模式,在這種模式下每次加鎖操作都會返回一個時間戳作為某種許可權憑證;每次解鎖操作都需要提供它對應的時間戳。如果一個執行緒在請求一個寫操作鎖的時候,這個鎖碰巧已經被一個讀操作持有,那麼這個讀操作的解鎖將會失效(因為時間戳已經失效)。這個時候應用程式需要從頭再來,也許要使用悲觀模式的鎖(時間戳鎖也有實現)。你需要自己搞定這一切,並且一個時間戳只能解鎖它對應的鎖——這一點必須非常小心。

  下面我們來看一下這種鎖的例項——

long stamp = lock.tryOptimisticRead(); // 非阻塞路徑——超級快
work(); // 我們希望不要有寫操作在這時發生
if (lock.validate(stamp)){
       //成功!沒有寫操作干擾 
}
else {
       //肯定同時有另外一個執行緒獲得了寫操作鎖,改變了時間戳
       //懶漢說——我們切換到開銷更大的鎖吧
	
            stamp = lock.readLock(); //這是傳統的讀操作鎖,會阻塞
       try {
                 //現在不可能有寫操作發生了
                 work();

       }
       finally {
            lock.unlock(stamp); // 使用對應的時間戳解鎖
       }
}

 併發加法器

  Java 8另一個出色的功能是併發“加法器”,它對大規模執行的程式碼尤其有意義。一種最基本的併發模式就是對一個計數器的讀寫。就其本身而言,現今處理這個問題有很多方法,但是沒有一種能比Java 8提供的方法高效或優雅。 

  到目前為止,這個問題是用原子類(Atomics)來解決的,它直接利用了CPU的“比較並交換”指令(CAS)來測試並設定計數器的值。問題在於當一條CAS指令因為競爭而失敗的時候,AtomicInteger類會死等,在無限迴圈中不斷嘗試CAS指令,直到成功為止。在發生競爭概率很高的環境中,這種實現被證明是非常慢的。

  來看Java 8的LongAdder。這一系列類為大量並行讀寫數值的程式碼提供了方便的解決辦法。使用超級簡單。只要初始化一個LongAdder物件並使用它的add()和intValue()方法來累加和取樣計數器。

  這和舊的Atomic類的區別在於,當CAS指令因為競爭而失敗時,Adder不會一直佔著CPU,而是為當前執行緒分配一個內部cell物件來儲存計數器的增量。然後這個值和其他待處理的cell物件一起被加到intValue()的結果上。這減少了反覆使用CAS指令或阻塞其他執行緒的可能性。

  如果你問你自己,什麼時候應該用併發加法器而不是原子類來管理計數器?簡單的答案就是——一直這麼做。

 並行排序

  正像併發加法器能加速計數一樣,Java 8還實現了一種簡潔的方法來加速排序。這個祕訣很簡單。你不再這麼做:

Array.sort(myArray);

  而是這麼做:

Arrays.parallelSort(myArray);

  這會自動把目標陣列分割成幾個部分,這些部分會被放到獨立的CPU核上去執行,再把結果合併起來。這裡唯一需要注意的是,在一個大量使用多執行緒的環境中,比如一個繁忙的Web容器,這種方法的好處就會減弱(降低90%以上),因為越來越多的CPU上下文切換增加了開銷。

 切換到新的日期介面

  Java 8引入了全新的date-time介面。當前介面的大多數方法都已被標記為deprecated,你就知道是時候推出新介面了。新的日期介面為Java核心庫帶來了易用性和準確性,而以前只能用Joda time才能達到這樣的效果(譯者注:Joda time是一個第三方的日期庫,比Java自帶的庫更友好更易於管理)。

  跟任何新介面一樣,好訊息是介面變得更優雅更強大。但不幸的是還有大量的程式碼在使用舊介面,這個短時間內不會有改變。

  為了銜接新舊介面,歷史悠久的Date類新增了toInstant()方法,用於把Date轉換成新的表示形式。當你既要享受新介面帶來的好處,又要兼顧那些只接受舊的日期表示形式的介面時,這個方法會顯得尤其高效。

 控制作業系統程式

  想在你的程式碼裡啟動一個作業系統程式,通過JNI呼叫就能完成——但這個東西總令人一知半解,你很有可能得到一個意想不到的結果,並且一路伴隨著一些很糟糕的異常。

  即便如此,這是無法避免的事情。但程式還有一個討厭的特性就是——它們搞不好就會變成殭屍程式。目前從Java中執行程式帶來的問題是,程式一旦啟動就很難去控制它。

  為了幫我們解決這個問題,Java 8在Process類中引入了三個新的方法

  1. destroyForcibly——結束一個程式,成功率比以前高很多。
  2. isAlive——查詢你啟動的程式是否還活著。
  3. 過載了waitFor(),你現在可以指定等待程式結束的時間了。程式成功退出後這個介面會返回,超時的話也會返回,因為你有可能要手動終止它。

  這裡有兩個關於如何使用這些新方法的好例子——

  如果程式沒有在規定時間內退出,終止它並繼續往前走。

if (process.wait(MY_TIMEOUT, TimeUnit.MILLISECONDS)){
//成功 }
else {
process.destroyForcibly();
}

  在你的程式碼結束前,確保所有的程式都已退出。殭屍程式會逐漸耗盡系統資源。

for (Process p : processes) {
       if (p.isAlive()) {
             p.destroyForcibly();
       }
}

 精確的數字運算

  數字溢位會導致一些討厭的bug,因為它本質上不會出錯。在一些系統中,整型值不停地增長(比如計數器),溢位的問題就尤為嚴重。在這些案例裡面,產品在演進階段執行得很好,甚至商用後的很長時間內也沒問題,但最終會出奇怪的故障,因為運算開始溢位,產生了完全無法預料的值。

  為了解決這個問題,Java 8為Math類新增了幾個新的“精確型”方法,以便保護重要的程式碼不受溢位的影響,它的做法是當運算超過它的精度範圍的時候,丟擲一個未檢查的ArithmeticException異常。

int safeC = Math.multiplyExact(bigA, bigB); 
// 如果結果超出+-2^31,就會丟擲ArithmeticException異常

  唯一不好的地方就是你必須自己找出可能產生溢位的程式碼。無論如何,沒有什麼自動的解決方案。但我覺得有這些介面總比沒有好。

 安全的隨機數發生器

  在過去幾年中Java一直因為安全漏洞而飽受詬病。無論是否合理,Java已經做了大量工作來加強虛擬機器和框架層,使之免受攻擊。如果隨機數來源於隨機性不高的種子,那麼那些用隨機數來產生金鑰或者雜湊敏感資訊的系統就更易受攻擊。

  到目前為止,隨機數發生演算法由開發人員來決定。但問題是,如果你想要的演算法依賴於特定的硬體、作業系統、虛擬機器,那你就不一定能實現它。這種情況下,應用程式傾向於使用更弱的預設發生器,這就使他們暴露在更大的風險下了。

  Java 8新增了一個新的方法叫SecureRandom.getInstanceStrong(),它的目標是讓虛擬機器為你選擇一個安全的隨機數發生器。如果你的程式碼無法完全掌控作業系統、硬體、虛擬機器(如果你的程式部署到雲或者PaaS上,這是很常見的),我建議你認真考慮一下使用這個介面。

 可選引用

  空指標就像“踢到腳趾”一樣——從你學會走路開始就伴隨著你,無論現在你有多聰明——你還是會犯這個錯。為了幫助解決這個老問題,Java 8引入了一個新模板叫Optional<T>。

  這個模板是從Scala和Hashkell借鑑來的,用於明確宣告傳給函式或函式返回的引用有可能是空的。有了它,過度依賴舊文件或者看過的程式碼經常變動的人,就不需要去猜測某個引用是否可能為空。

Optional<User> tryFindUser(int userID) {

  或

void processUser(User user, Optional<Cart> shoppingCart) {

  Optional模板有一套函式,使得采樣它更方便,比如isPresent()用來檢查這個值是不是非空,或者ifPresent()你可以傳遞一個Lambda函式過去,如果isPresent()返回true,這個Lambda函式就會被執行。不好的地方就跟Java 8的新日期介面一樣,等這種模式逐漸流行,滲透到我們使用的庫中和日常設計中,需要時間和工作量。

  用新的Lambda語法列印Optional值:

value.ifPresent(System.out::print);

 關於作者

  Tal Weiss是Takipi的CEO。過去十五年中,Tal一直在設計大規模的實時的Java和C++應用。可是他仍然陶醉於分析有挑戰性的bug,以及評估Java程式碼的效能。業餘時間他喜歡爵士鼓。

  英文原文: 8 Great Java 8 Features No One's Talking about

相關文章