使用Java中成型的框架來幫助我們開發(fā)并發(fā)應(yīng)用即可以節(jié)省構(gòu)建項(xiàng)目的時(shí)間,也可以提高應(yīng)用的性能。
創(chuàng)新互聯(lián)專注為客戶提供全方位的互聯(lián)網(wǎng)綜合服務(wù),包含不限于成都網(wǎng)站設(shè)計(jì)、網(wǎng)站建設(shè)、輪臺(tái)網(wǎng)絡(luò)推廣、成都小程序開發(fā)、輪臺(tái)網(wǎng)絡(luò)營銷、輪臺(tái)企業(yè)策劃、輪臺(tái)品牌公關(guān)、搜索引擎seo、人物專訪、企業(yè)宣傳片、企業(yè)代運(yùn)營等,從售前售中售后,我們都將竭誠為您服務(wù),您的肯定,是我們最大的嘉獎(jiǎng);創(chuàng)新互聯(lián)為所有大學(xué)生創(chuàng)業(yè)者提供輪臺(tái)建站搭建服務(wù),24小時(shí)服務(wù)熱線:18982081108,官方網(wǎng)址:www.cdcxhl.com
Java對(duì)象實(shí)例的鎖一共有四種狀態(tài):無鎖,偏向鎖,輕量鎖和重量鎖。原始脫離框架的并發(fā)應(yīng)用大部分都需要手動(dòng)完成加鎖釋放,最直接的就是使用synchronized和volatile關(guān)鍵字對(duì)某個(gè)對(duì)象或者代碼塊加鎖從而限制每次訪問的次數(shù),從對(duì)象之間的競爭也可以實(shí)現(xiàn)到對(duì)象之間的協(xié)作。但是這樣手動(dòng)實(shí)現(xiàn)出來的應(yīng)用不僅耗費(fèi)時(shí)間而且性能表現(xiàn)往往又有待提升。順帶一提,之前寫過一篇文章介紹我基于Qt和Linux實(shí)現(xiàn)的一個(gè)多線程下載器(到這里不需要更多了解這個(gè)下載器,請(qǐng)直接繼續(xù)閱讀),就拿這個(gè)下載器做一次反例:
首先,一個(gè)下載器最愚蠢的問題之一就是把下載線程的個(gè)數(shù)交由給用戶去配置。比如一個(gè)用戶會(huì)認(rèn)為負(fù)責(zé)下載的線程個(gè)數(shù)是越多越好,干脆配置了50個(gè)線程去下載一份任務(wù),那么這個(gè)下載器的性能表現(xiàn)甚至?xí)蝗缫粋€(gè)單進(jìn)程的下載程序。最直接的原因就是JVM花費(fèi)了很多計(jì)算資源在線程之間的上下文切換上面,對(duì)于一個(gè)并發(fā)的應(yīng)用:如果是CPU密集型的任務(wù),那么良好的線程個(gè)數(shù)是實(shí)際CPU處理器的個(gè)數(shù)的1倍;如果是I/O密集型的任務(wù),那么良好的線程個(gè)數(shù)是實(shí)際CPU處理器個(gè)數(shù)的1.5倍到2倍(具體記不清這句話是出于哪里了,但還是可信的)。不恰當(dāng)?shù)膱?zhí)行線程個(gè)數(shù)會(huì)給線程抖動(dòng),CPU抖動(dòng)等隱患埋下伏筆。如果,重新開發(fā)那么我一定會(huì)使用這種線程池的方法使用生產(chǎn)者和消費(fèi)者的關(guān)系模式,異步處理HTTP傳輸過來的報(bào)文。
其次,由于HTTP報(bào)文的接受等待的時(shí)間可能需要等待很久,然而處理報(bào)文解析格式等等消耗的計(jì)算資源是相當(dāng)較小的。同步地處理這兩件事情必然會(huì)使下載進(jìn)程在一段時(shí)間內(nèi)空轉(zhuǎn)或者阻塞,這樣處理也是非常不合理的。如果重新開發(fā),一定要解耦HTTP報(bào)文的接收和HTTP報(bào)文的解析,這里盡管也可以使用線程池去進(jìn)行處理,顯而易見由于這樣去做的性能提升其實(shí)是很小的,所以沒有必要去實(shí)現(xiàn),單線程也可以快速完成報(bào)文的解析。
Okay,回到主題,總而言之是線程之間的上下文切換導(dǎo)致了性能的降低。那么具體應(yīng)該怎么樣去做才可以減少上下文的切換呢?
1. 無鎖并發(fā)編程
多線程競爭鎖時(shí),會(huì)引起上下文切換,所以多線程處理數(shù)據(jù)時(shí),可以用一些辦法來避免使用鎖,如將數(shù)據(jù)的ID按照Hash算法取模分段,不同的線程去處理不同段的數(shù)據(jù)。
2. CAS算法
Java的Atomic包內(nèi)使用CAS算法來更新數(shù)據(jù),而不需要加鎖(但是線程的空轉(zhuǎn)還是存在)。
3. 使用最少線程
避免創(chuàng)建不需要的線程,比如任務(wù)很少,但是創(chuàng)建很多線程來處理,這樣會(huì)造成大量線程都處于等待狀態(tài)。
4. 協(xié)程
在單線程里實(shí)現(xiàn)多任務(wù)的調(diào)度,并在單線程里維持多個(gè)任務(wù)間的切換。
總的來說使用Java線程池會(huì)帶來以下3個(gè)好處:
1. 降低資源消耗: 通過重復(fù)利用已創(chuàng)建的線程降低線程創(chuàng)建和銷毀造成的消耗。
2. 提高響應(yīng)速度: 當(dāng)任務(wù)到達(dá)時(shí),任務(wù)可以不需要等到線程創(chuàng)建就能立即執(zhí)行。
3. 提高線程的可管理性: 線程是稀缺資源,如果無限制的創(chuàng)建。不僅僅會(huì)降低系統(tǒng)的穩(wěn)定性,使用線程池可以統(tǒng)一分配,調(diào)優(yōu)和監(jiān)控。但是要做到合理的利用線程池。必須對(duì)于其實(shí)現(xiàn)原理了如指掌。
線程池的實(shí)現(xiàn)原理如下圖所示:
Executor框架的兩級(jí)調(diào)度模型:
在HotSpot VM線程模型中,Java線程被一對(duì)一的映射為本地操作系統(tǒng)線程,Java線程啟動(dòng)時(shí)會(huì)創(chuàng)建一個(gè)本地操作系統(tǒng)線程,當(dāng)該Java線程終止時(shí),這個(gè)操作系統(tǒng)也會(huì)被回收。操作系統(tǒng)會(huì)調(diào)度并將它們分配給可用的CPU。
在上層,Java多線程程序通常把應(yīng)用分解為若干個(gè)任務(wù),然后把用戶級(jí)的調(diào)度器(Executor框架)將這些映射為固定數(shù)量的線程;在底層,操作系統(tǒng)內(nèi)核將這些線程映射到硬件處理器上。這種兩級(jí)調(diào)度模型實(shí)質(zhì)是一種工作單元和執(zhí)行機(jī)制的解偶。
Fork/Join框架的遞歸調(diào)度模型:
要提高應(yīng)用程序在多核處理器上的執(zhí)行效率,只能想辦法提高應(yīng)用程序的本身的并行能力。常規(guī)的做法就是使用多線程,讓更多的任務(wù)同時(shí)處理,或者讓一部分操作異步執(zhí)行,這種簡單的多線程處理方式在處理器核心數(shù)比較少的情況下能夠有效地利用處理資源,因?yàn)樵谔幚砥骱诵谋容^少的情況下,讓不多的幾個(gè)任務(wù)并行執(zhí)行即可。但是當(dāng)處理器核心數(shù)發(fā)展很大的數(shù)目,上百上千的時(shí)候,這種按任務(wù)的并發(fā)處理方法也不能充分利用處理資源,因?yàn)橐话愕膽?yīng)用程序沒有那么多的并發(fā)處理任務(wù)(服務(wù)器程序是個(gè)例外)。所以,只能考慮把一個(gè)任務(wù)拆分為多個(gè)單元,每個(gè)單元分別得執(zhí)行最后合并每個(gè)單元的結(jié)果。一個(gè)任務(wù)的并行拆分,一種方法就是寄希望于硬件平臺(tái)或者操作系統(tǒng),但是目前這個(gè)領(lǐng)域還沒有很好的結(jié)果。另一種方案就是還是只有依靠應(yīng)用程序本身對(duì)任務(wù)經(jīng)行拆封執(zhí)行。
Fork/Join模型乍看起來很像借鑒了MapReduce,但是具體不敢肯定是什么原因,實(shí)際用起來的性能提升是遠(yuǎn)不如Executor的。甚至在遞歸棧到了十層以上的時(shí)候,JVM會(huì)卡死或者崩潰,從計(jì)算機(jī)的物理原理來看,F(xiàn)ork/Join框架實(shí)際效能也沒有想象中的那么美好,所以這篇只稍微談一下,不再深究。
Executor框架主要由三個(gè)部分組成:任務(wù),任務(wù)的執(zhí)行,異步計(jì)算的結(jié)果。
主要的類和接口簡介如下:
1. Executor是一個(gè)接口,它將任務(wù)的提交和任務(wù)的執(zhí)行分離。
2. ThreadPoolExecutor是線程池的核心,用來執(zhí)行被提交的類。
3. Future接口和實(shí)現(xiàn)Future接口的FutureTask類,代表異步計(jì)算的結(jié)果。
4. Runnable接口和Callable接口的實(shí)現(xiàn)類,都可以被ThreadPoolExecutor或其他執(zhí)行。
先看一個(gè)直接的例子(用SingleThreadExecutor來實(shí)現(xiàn),具體原理下面會(huì)闡述):
?1?public?class?ExecutorDemo?{ ?2? ?3? ?4?public?static?void?main(String[]?args){ ?5? ?6?//ExecutorService?fixed=?Executors.newFixedThreadPool(4); ?7?ExecutorService?single=Executors.newSingleThreadExecutor(); ?8?//ExecutorService?cached=Executors.newCachedThreadPool(); ?9?//ExecutorService?sched=Executors.newScheduledThreadPool(4); 11? 12?Callable?callable=Executors.callable(new?Runnable()?{ 13?@Override 14?public?void?run()?{ 15?for(int?i=0;i<100;i++){ 16?try{ 17?System.out.println(i); 18?}catch(Throwable?e){ 19?e.printStackTrace(); 20?} 21?} 22?} 23?},"success"); 24? //這里抖了個(gè)機(jī)靈,用Executors工具類的callable方法將一個(gè)匿名Runnable對(duì)象裝飾為Callable對(duì)象作為參數(shù) 25?Future ?f=single.submit(callable); 26?try?{ 27?System.out.println(f.get()); 28?single.shutdown(); 29?}catch(Throwable?e){ 30?e.printStackTrace(); 31?} 32?} 33?}
如代碼中所示,常用一共有四種Exector實(shí)現(xiàn)類通過Executors的工廠方法來創(chuàng)建Executor的實(shí)例,其具體差別及特點(diǎn)如下所示:
1. FixedThreadPool
這個(gè)是我個(gè)人最常用的實(shí)現(xiàn)類,在Java中最直接的使用方法就是和 Runtime.getRuntime().availableProcessors() 一起使用分配處理器個(gè)數(shù)個(gè)的Executor。內(nèi)部結(jié)構(gòu)大致如下:
創(chuàng)造實(shí)例的函數(shù)為: Executors.newFixedThreadPool(int nThread);
在JDK1.7里java.util.concurrent包中的源碼中隊(duì)列使用的是new LinkedBlockingQueue
2. SingleThreadExecutor
和 Executors.newFixedThreadPool(1) 完全等價(jià)。
3. CachedThreadPool
和之前兩個(gè)實(shí)現(xiàn)類完全不同的是,這里使用SynchronousQueue替換LinkedBlockingQueue。簡單提一下SynchronousQueue是一個(gè)沒有容量的隊(duì)列,一個(gè)offer必須對(duì)應(yīng)一個(gè)poll,當(dāng)然所謂poll操作是由實(shí)際JVM工作線程來進(jìn)行的,所以對(duì)于使用開發(fā)者來講,這是一個(gè)會(huì)因?yàn)楣ぷ骶€程飽和而阻塞的線程池。(這個(gè)和java.util.concurrent.Exchanger的作用有些相似,但是Exchanger只是對(duì)于兩個(gè)JVM線程的,而SynchronousQueue的阻塞機(jī)制是多個(gè)生產(chǎn)者和多個(gè)消費(fèi)者而言的。)
4. ScheduledThreadPoolExecutor
這個(gè)實(shí)現(xiàn)類內(nèi)部使用的是DelayQueue。DelayQueue實(shí)際上是一個(gè)優(yōu)先級(jí)隊(duì)列的封裝。時(shí)間早的任務(wù)會(huì)擁有更高的優(yōu)先級(jí)。它主要用來在給定的延遲之后運(yùn)行任務(wù),或者定期執(zhí)行任務(wù)。ScheduledThreadPoolExecutor的功能與Timer類似,但ScheduledThreadPoolExecutor比Timer更加靈活,而且可以有多個(gè)后臺(tái)線程在構(gòu)造函數(shù)之中指定。
Future接口和ListenableFurture接口
Future接口為異步計(jì)算取回結(jié)果提供了一個(gè)存根(stub),然而這樣每次調(diào)用Future接口的get方法取回計(jì)算結(jié)果往往是需要面臨阻塞的可能性。這樣在最壞的情況下,異步計(jì)算和同步計(jì)算的消耗是一致的。Guava庫中因此提供一個(gè)非常強(qiáng)大的裝飾后的Future接口,使用觀察者模式為在異步計(jì)算完成之后馬上執(zhí)行addListener指定一個(gè)Runnable對(duì)象,從實(shí)現(xiàn)“完成立即通知”。