咱們都能依稀記得,在2005年Sun發佈了代號爲Tiger的Java 5。在其衆多的特性之中, JVM的改進和java.util.concurrent包的出現無疑很是引人注目。改進後的JVM可使用低層機器指令取代鎖,精化了互斥訪問的粒度,提升了系統的可伸縮性和活性。而concurrent包提供了大量線程和鎖之上的併發抽象,好比線程池、閉鎖、信號量、關卡等。這些能夠幫助開發者快速地構建出高效、可伸縮的系統來。就在同一年,還發生了另外一件影響深遠的事件:AMD首次發佈了其雙核CPU,從而打響了AMD與Intel兩大芯片廠商的多核之爭。java
當Java遇到多核程序員
早在十多年前,IBM、Sun與HP就已經設計出了雙核處理器,好比IBM於2001年推出的基於雙核的POWER4處理器和Sun的UltraSPARC芯片,但這些雙核處理器都是用於高端的RISC領域,價格高昂,被大衆使用的X86並無享受到它帶來的性能優點。直到Intel和AMD相繼推出本身的雙核處理器後,X86領域纔算是有了本身的多核架構。編程
所謂雙核處理器,簡單地說就是在一塊CPU基板上集成兩個處理器核心,並經過並行總線將各處理器核心鏈接起來。多核並非一個新概念,而只是CMP(Chip Multi Processors,單芯片多處理器)中最基本、最簡單、最容易實現的一種類型。其實在RISC處理器領域,多核都早已經實現。安全
多核與單核的區別在於,前者可讓程序真正地「同時」執行,而不是多個進程輪流使用CPU,從而給用戶形成「多個程序正在同時執行」的假象。簡單地說,「併發」就是爲了讓程序運行得更快。在之前,達到這個目的的手段一般是依賴CPU時鐘頻率的提高。然而普通單核心處理器的頻率難於提高,性能沒有質的飛躍。因爲頻率難於提高,Intel在發佈3.8GHz的產品之後只得宣佈中止4GHz的產品計劃;而AMD在實際頻率超過2GHz之後也沒法大幅度提高,3GHz成爲了AMD沒法逾越的一道坎。所以,CPU內部開始出現了兩個、四個甚至更多的內核。架構
爲了充分發揮每個核的效用,應用程序須要多個線程同時運行來保持CPU核的忙碌。Java能夠幫助你在多核系統上構建良好的應用程序。它能夠方便地、相對便宜地建立線程,這很是重要。若是建立線程的開銷比線程完成工做的開銷還要大,那麼併發將變得毫無心義。concurrent包中提供的併發構建塊很是豐富,幾乎覆蓋了全部併發編程中用到的工具。不只如此,Java還在快速地演變着,以適應更高併發性系統的構建,例如,Java 7中即將加入ForkJoin框架,按照Doug Lea的描述,它專門適用於「>32個CPU(內核)的系統」。併發
除了這些API層面的支持外,Java在併發編程上的底層平臺上顯得更加野心勃勃,由於Java有本身的存儲模型,這個存儲模型早在Java 1.1時代就存在了,儘管當時還不完善,但綷-過屢次修補和改進,已綷-變得很是成熟了。Java存儲模型能夠抹平不一樣硬件平臺提供的存儲模型的差別,嚴格地定義了線程間通信的規範。例如,對於C語言來講,相同的代碼在X86和PowerPC上會有不一樣的語義。框架
細心的讀者可能注意到,Java不過是這場變革的一個突出的表明而已。不少語言都在不一樣程度上調整本身,以適應2.0計算時代對併發的須要。在不久,當有人問C#之父Anders Hejlsberg,「將來幾年內語言的發展方向在何處」時,Anders表示「要處理好多核的問題,並提供一個更好的併發模型」。Erlang語言最近也受到愈來愈多的關注,這說明人們迫切須要一個強大而又充分簡單的工具來解決併發編程的挑戰。編程語言
以Java語言爲表明的編程語言在面對併發時代所作出的努力是使人激動的,可是相比於硬件,軟件的發展老是滯後的,人們老是等意識到軟件出現問題了纔開始着手改進。2.0時代的併發計算正在向桌面和客戶端轉移,可是有多少人作好準備了?因而有人開始驚呼:狼來了?模塊化
關於「狼來了」的討論,最好的結論就是不管狼是否真的會來,羊圈的籬笆仍然都要修理。這裏的「籬笆」就是指併發編程技術。因爲是底層的計算平臺正在發生變化,所以不只僅是開發者,包括需求分析人員、設計者、程序員和測試者都應該在工做時考慮到併發帶來的影響。好比:高併發
咱們常常要思考,如何才能得到一個最佳的程序粒度,同時保證它們最大限度地彼此隔離,從而能夠簡單地分配到不一樣的處理器單元上?線程間通信的內容有哪些?等等。要想很好地回答這些問題,僅靠程序員或者設計者是不夠的。它須要從獲取需求開始,就着力對任務進行劃分。系統對於併發性的要求,很難在開發的中後期經過「重構」來引入,所以必須在設計之初就給予關注。評價軟件有不少標準,好比可擴展性、模塊化、鬆耦合等等,今天還要考慮軟件是否有足夠的併發性,以充分利用底層的計算資源。它必將成爲衡量軟件質量的重要標準之一。
編寫併發程序須要面對不少挑戰。尤爲是多核的流行,它使程序中能夠有多個線程真正地 「同時」運行。所以開發者要面對的一個最大挑戰是劃分任務。也許你須要對數據進行劃分,清晰地識別出任務邊界,還要儘量地讓每一個任務在執行時只使用本身的數據。若是不一樣的線程要共享數據,問題將迅速變得複雜。你沒法再像之前那樣,只要等上幾個月,就能夠換上更強勁的處理器,從而讓你的程序運行的更快。今天,「免費的午飯」已經結束了。能不能把並行化的工做徹底交給操做系統和編譯器呢?這是一個充滿誘惑力的願景-可是,並行化的工做如今沒法自動實現,將來也只能在必定程度上有所緩解,而不可能所有交由機器完成。並行化過程的重點在於分解程序的任務流和執行流,這最終被歸結爲數學問題——一個無解的數學問題!面對多核,咱們當然不能中止讓機器變得更聰明的努力,可是對於大多數人來講,更重要的仍是充實本身的知識儲備,適應新的思惟方式,就像當年從過程化編程過渡到面向對象編程時所作的那樣。
併發程序難以開發是事實,同時測試與調試更加困難。由於併發錯誤一般更加隱蔽,它們只有在高負荷和一些特定時序下才會出現,並且難以再現。所以,併發程序的測試須要更多的投入、更好的工具、更精巧的測試策略。一般,並行程序要對須要測試兩個要點:性能與安全性。安全性包括程序是否「作了該作的事」、以及是否「沒作不應作的事」兩個方面。而性能是指是否在規定的時間裏作了正確的事。完整的測試計劃應該包括:測試計劃、單元測試、代碼審查和靜態分析等。測試計劃應該確保後期的測試行爲得以貫徹,同時還要衆-調測試過程當中須要的各類資源。併發程序測試的難度遠遠高於串行程序測試。以單元測試爲例,咱們幾乎沒法只使用惟一的線程完成併發測試,至少須要兩個線程。然而在JUnit框架,只能識別其自身的線程,對於其餘線程拋出的斷言失敗或異常,毫無察覺。所以,測試者在編寫時就要編寫大量衆-調線程的代碼,這自己就會帶來新的bug,更糟糕的是,不良的測試還會掩蓋被測代碼中的錯誤。
併發,釋放2.0的力量
2.0時代是軟件空前繁榮的時代,軟件將幫助愈來愈多的人完成愈來愈多工做。咱們也一直在探討解決2.0時代的計算方法。那麼,無限度地提高CPU核的時鐘頻率或者在一個CPU內加入更多的內核,就是2.0時代的計算方法了麼?不是。2.0時代的計算方法不只要有強勁的計算硬件作基礎,也要有更優秀、更復雜的軟件作後盾。單靠硬件的發展是沒法知足計算量的激增的,軟件也必須作出相應的調整。
在多核系統上進行併發編程仍然很困難,它不符合人類的思惟方式。縱觀計算機軟件開發的歷史,老是伴隨着新技術推出、開發者學習消化的旋律。從最先的幾十個機器指令,到今天的結構化語言、面向對象、AOP--
2.0時代的另外一個效應是,它把並行計算和併發編程擺在了更多的程序員面前。之前,併發編程仍是計算機科學家的專利,不少開發者都將它視爲「禁地」。現在,除了複雜的商業應用和龐大的科學計算外,桌面端、各類終端都在逐漸地走向高併發的運行環境。咱們能夠學習新的技術、新的思路或者新的語言,好比,在上世紀80年代誕生於愛立信實驗室的Erlang語言,近期就表現出極大的活力。在Erlang中,若是要訪問共享數據,就須要向數據的擁有者發送一條消息並等待迴應,這種方法在構造高可用的併發系統時已綷-取得了極大的成功。尤爲是咱們已經習慣了基於對象的抽象。要讓併發變得容易,就要放棄一些抽象。在此咱們不去討論什麼是正確的取捨,可是咱們正在苦於這樣的幻想:「能不能不作任何取捨呢?」
相對於Web2.0和企業2.0的波濤洶涌來講,計算2.0顯得有些波瀾不驚。可是,這種處於底層的深入變化,將會完全改變上層應用的面貌。多核的普及,將使並行/併發的大行其道,企業或者我的只有快速抓住這個趨勢,才能順利地暢遊於軟件的2.0時代。(文/韓鍇 方妙)
韓 Works諮詢師。從事Eclipse插件與客戶端軟件開發,對併發程序設計、敏捷過程具備濃厚的興趣。