史廣



摘要:隨著Java并發(concurrem)工具包的推出,并發程序的開發方式得到了極大的優化。較之以往的多線程設計機制,論文從四個方面深入探討了并發(conCurrent)工具包是如何提升并發編程效能的。
關鍵詞:并發;多線程;java
doi:10.16083/j.cnki.1671-1580.2016.08.025
中圖分類號:TP311
文獻標識碼:A
文章編號:1671-1580(2016)08-0078-04
在JDKl.5出現之后,Sun推出了并發(concur-rent)工具包以簡化并發編程,它為開發者提供了更為實用的并發程序模型,使得編寫高效、易維護、結構清晰的Java多線程程序更為容易。
一、簡化了線程的管理,提升了線程的執行效能
在JDKl.5之前,使用newThread()方式定義線程,需要考慮線程的創建、結束和結果的獲取等諸多細節。尤其在需要很多線程時,線程的管理就會變得比較困難。不僅如此,使用newThread()方式定義線程,在時間和空間效率方面都存在不足。比如,定義好的線程不能重復利用;每次使用線程都需要向系統申請資源重新創建。另外,線程的創建和啟動都需要一定的時間,這也會影響程序執行效率。
線程池(ThreadPool)是并發(Concurrent)工具包中引入的機制,它對以上問題進行了很好的優化。線程池通過一個緩存池的空間預先創建了一部分線程,在我們需要使用的時候就從里面直接將線程資源取出來使用。在線程使用完畢之后,線程池可以將線程回收進行重復利用。因此,在對性能要求較高或線程請求較多的情況下,線程池是一個很理想的選擇。
上述程序創建了一個可緩存線程池(cachedThread Pool):只需通過循環的方式,便可把想要完成的任務數量傳遞給線程池,線程池會創建盡可能多的必須線程來并行執行。一旦前面的線程執行結束后可以被重復使用。
除了可緩存線程池(cached Thread Pool),定長線程池(Fix Thread Pool)可以創建固定數量的線程。在線程都被使用之后,后續申請使用的線程都會被阻塞。如果我們將上述代碼中的new.CachedThreadPool改為newFixedThreadPool(2),括號中的數字2,表示創建了兩個線程。下面的代碼不變,則相等于把5個任務交給兩個線程來完成。這就意味著,每一個線程完成一次任務后,并不會就此消逝,而是繼續完成剩下的任務,直到所有任務完成??梢娋€程池確實簡化了線程的管理,提升了線程的執行效能
二、強化了對多線程并發的控制能力。簡化了控制線程間協調合作的方法
在并發(concurrent)工具包中,包含了一些同步輔助類,使得開發者能夠更加輕松的對多線程進行協調控制,豐富了多個線程間協作的方式。下面我們以閉鎖(countDownLatch)和信號量(sema-phore)為例進行說明。
1.閉鎖(countDownLatch)是一個并發構造,它允許一個或多個線程等待一系列指定操作的完成。閉鎖(CountDownLatch)以一個給定的計數初始化,每調用一次countDown(),這一數量就減一,然后通過調用await()方法,將阻塞線程,使其等待直到計數到達零。
在主方法中,除t1和t2線程外,還有語句“Sys.tern.out.println(“HelloWorld”)”所在的主線程,三個線程本來沒有執行的先后順序,但是如果運行代碼,會發現主線程只在t1和t2完成后,才會執行。這就是因為我們對線程執行的順序進行了人工干預,await()方法會一直阻塞主線程,直到countDown()方法將計數倒數至0。
2.信號量(Semaphore),有時被稱為信號燈,它負責協調各個線程,以保證它們能夠正確、合理的使用公共資源。信號量可以控制某個資源可被同時訪問的個數,拿到信號量的線程可以進入代碼,否則就等待,通過acquire()和release()獲取和釋放訪
從上面代碼中看出線程執行的任務是連接數據庫。當線程成功連接數據庫兩秒后,連接將自動斷開。重要的是,由于服務器資源有限,在給定時間內,可以提供的連接數必須受到嚴格控制。如果此時有200個線程同時訪問數據庫資源,但服務器在同一時問內只提供10個連接端口,這種情況就需要信號量來實現對線程的有效管控。
在上述代碼中,將信號量定義為10,每條線程在進行數據庫連接時,必須首先通過acquire()獲得信號,并且在斷開數據庫連接后,通過release()釋放獲得的信號,以便其他線程獲取。也就是說,雖然有200個線程同時想要進行數據庫連接,但是在同一時間內,只能有10個線程獲得信號來并發執行??梢钥闯?,信號量的應用,豐富了我們對線程的管控方式,也簡化了操作過程。
三、提供了多個具有線程安全性的類
在iaval.5出現之前,多線程訪問共享數據時造成的數據訪問沖突問題,往往需要耗費程序員大量時間進行調試規避。但是在并發(Concurrent)工具包中,提供的很多類本身就是線程安全的,這樣大大簡化了并發編程的難度。比如前文中提到的閉鎖就是一個線程安全類。在閉鎖的示例代碼中,對象latch被兩條線程并發共享,且沒有synchronized關鍵字鎖定,但程序并不會出現訪問沖突。除Count.DownLatch以外,阻塞隊列(BlockingQueue)的線程安全性也為開發并發程序帶來了極大方便。
在經典的“生產者”(producer)和“消費者”(con-sumer)模型中,通過隊列可以很便利地實現兩者之問的數據共享。但是,在生產者和消費者數據處理速度不匹配,且生產者產出數據的速度遠大于消費者消費速度的情況下,生產者必須在數據累積到一定程度時暫停下來(阻塞生產者線程),以便消費者線程把累積的數據處理完畢。然而,在并發(concur-rent)工具包發布以前,開發者不僅需要考慮所有上述細節,還要兼顧效率和線程安全,開發的復雜度可見一斑。阻塞隊列很好地解決了如何在“生產者”(producer)和“消費者”(consumer)之間高效安全“傳輸”數據的問題。阻塞隊列是一個高效并且線程安全的隊列類,它為我們快速搭建高質量的多線程程序帶來極大的便利。
上面是一個典型的“生產者”(producer)和“消費者”(consumer)模型,生產者和消費者共享阻塞隊列queue。生產者的生產速度遠大于消費者,因為消費者每取出一個數據需要等待100毫秒。幸運的是,阻塞隊列首先是一個線程安全隊列,其次,當生產者將隊列長度填充到10時,put()方法會進入等待狀態,同理,當隊列長度縮減到0時take()也會進行等待。這就使得使用阻塞隊列開發多線程常見模型的復雜度大大降低。
四、針對并發程序可能引起的死鎖問題給出了更為便捷的解決方案
當線程需要同時獲得多個互斥鎖才可運行時,如果有多條線程并行且獲取互斥鎖的順序不同,就可能引發死鎖(DeadLock)現象。
在并發(concurrent)工具包中,引入了重入鎖(Re-entrantlock)。重入鎖的基本原理和synchronized語句塊相似,都是通過加互斥鎖的方式限定線程的行為,它們的第一個不同點在于,首先當需要加一個以上的互斥鎖時,使用Reentrant lock避免了像synchronized語句塊一樣的嵌套。更為重要的是,re-entrant lock有tryLock()功能,它使得線程在需要同時獲得多個互斥鎖的情況下,具備了更加靈活的應對機制。
上述代碼中,acquireLocks()方法的作用就是幫助線程分別獲得兩個互斥鎖,當只獲得一個,另一個獲取失敗的情況下,由于tryLock()方法的作用,使得程序可以在此情況下,放棄已經獲得的互斥鎖,以便其它線程同時獲取。這也就很巧妙的避免了死鎖現象的出現。
五、結語
并發(concurrent)工具包的推出,使得原來很麻煩的并發處理得以輕松完成,它實現了很多{avathread原生API很費時才能實現的功能。并發(Con.current)工具包的優點可以概括為:簡化了線程的管理,提升了線程的執行效能;強化了對多線程并發的控制能力,簡化了控制線程問協調合作的方法;提供了多個具有線程安全性的類;針對并發程序可能引起的死鎖等問題給出了更為便捷的解決方案。當然,隨著iava語言的發展,我們相信新的工具包還會不斷更新,來滿足日益繁雜的開發需求。