這篇文章主要介紹了JAVA多線程的使用場景與注意事項有哪些,具有一定借鑒價值,感興趣的朋友可以參考下,希望大家閱讀完這篇文章之后大有收獲,下面讓小編帶著大家一起了解一下。
目前成都創(chuàng)新互聯(lián)公司已為成百上千的企業(yè)提供了網(wǎng)站建設(shè)、域名、網(wǎng)絡(luò)空間、網(wǎng)站運營、企業(yè)網(wǎng)站設(shè)計、文安網(wǎng)站維護(hù)等服務(wù),公司將堅持客戶導(dǎo)向、應(yīng)用為本的策略,正道將秉承"和諧、參與、激情"的文化,與客戶和合作伙伴齊心協(xié)力一起成長,共同發(fā)展。
線程
Thread
第一類就是Thread類。大家都知道有兩種實現(xiàn)方式。第一可以繼承Thread覆蓋它的run方法;第二種是實現(xiàn)Runnable接口,實現(xiàn)它的run方法;而第三種創(chuàng)建線程的方法,就是通過線程池。
我們的具體代碼實現(xiàn),就放在run方法中。
我們關(guān)注兩種情況。一個是線程退出條件,一個是異常處理情況。
線程退出
有的run方法執(zhí)行完成后,線程就會退出。但有的run方法是永遠(yuǎn)不會結(jié)束的。結(jié)束一個線程肯定不是通過Thread.stop()方法,這個方法已經(jīng)在java1.2版本就廢棄了。所以我們大體有兩種方式控制線程。
定義退出標(biāo)志放在while中
代碼一般長這樣。
private volatile boolean flag= true; public void run() { while (flag) { } }
標(biāo)志一般使用volatile進(jìn)行修飾,使其讀可見,然后通過設(shè)置這個值來控制線程的運行,這已經(jīng)成了約定俗成的套路。
使用interrupt方法終止線程
類似這種。
while(!isInterrupted()){……}
對于InterruptedException,比如Thread.sleep所拋出的,我們一般是補(bǔ)獲它,然后靜悄悄的忽略。中斷允許一個可取消任務(wù)來清理正在進(jìn)行的工作,然后通知其他任務(wù)它要被取消,最后才終止,在這種情況下,此類異常需要被仔細(xì)處理。
interrupt方法不一定會真正”中斷”線程,它只是一種協(xié)作機(jī)制。interrupt方法通常不能中斷一些處于阻塞狀態(tài)的I/O操作。比如寫文件,或者socket傳輸?shù)?。這種情況,需要同時調(diào)用正在阻塞操作的close方法,才能夠正常退出。
interrupt系列使用時候一定要注意,會引入bug,甚至死鎖。
異常處理
java中會拋出兩種異常。一種是必須要捕獲的,比如InterruptedException,否則無法通過編譯;另外一種是可以處理也可以不處理的,比如NullPointerException等。
在我們的任務(wù)運行中,很有可能拋出這兩種異常。對于第一種異常,是必須放在try,catch中的。但第二種異常如果不去處理的話,會影響任務(wù)的正常運行。
有很多同學(xué)在處理循環(huán)的任務(wù)時,沒有捕獲一些隱式的異常,造成任務(wù)在遇到異常的情況下,并不能繼續(xù)執(zhí)行下去。如果不能確定異常的種類,可以直接捕獲Exception或者更通用的Throwable。
while(!isInterrupted()){ try{ …… }catch(Exception ex){ …… } }
同步方式
java中實現(xiàn)同步的方式有很多,大體分為以下幾種。
synchronized 關(guān)鍵字
wait、notify等
Concurrent包中的ReentrantLock
volatile關(guān)鍵字
ThreadLocal局部變量
生產(chǎn)者、消費者是wait、notify最典型的應(yīng)用場景,這些函數(shù)的調(diào)用,是必須要放在synchronized代碼塊里才能夠正常運行的。它們同信號量一樣,大多數(shù)情況下屬于炫技,對代碼的可讀性影響較大,不推薦。關(guān)于ObjectMonitor相關(guān)的幾個函數(shù),只要搞懂下面的圖,就基本ok了。
使用ReentrantLock最容易發(fā)生錯誤的就是忘記在finally代碼塊里關(guān)閉鎖。大多數(shù)同步場景下,使用Lock就足夠了,而且它還有讀寫鎖的概念進(jìn)行粒度上的控制。我們一般都使用非公平鎖,讓任務(wù)自由競爭。非公平鎖性能高于公平鎖性能,非公平鎖能更充分的利用cpu的時間片,盡量的減少cpu空閑的狀態(tài)時間。非公平鎖還會造成餓死現(xiàn)象:有些任務(wù)一直獲取不到鎖。
synchronized通過鎖升級機(jī)制,速度不見得就比lock慢。而且,通過jstack,能夠方便的看到其堆棧,使用還是比較廣泛。
volatile總是能保證變量的讀可見,但它的目標(biāo)是基本類型和它鎖的基本對象。假如是它修飾的是集合類,比如Map,那么它保證的讀可見是map的引用,而不是map對象,這點一定要注意。
synchronized和volatile都體現(xiàn)在字節(jié)碼上(monitorenter、monitorexit),主要是加入了內(nèi)存屏障。而Lock,是純粹的java api。
ThreadLocal很方便,每個線程一份數(shù)據(jù),也很安全,但要注意內(nèi)存泄露。假如線程存活時間長,我們要保證每次使用完ThreadLocal,都調(diào)用它的remove()方法(具體來說是expungeStaleEntry),來清除數(shù)據(jù)。
關(guān)于Concurrent包
concurrent包是在AQS的基礎(chǔ)上搭建起來的,AQS提供了一種實現(xiàn)阻塞鎖和一系列依賴FIFO等待隊列的同步器的框架。
線程池
最全的線程池大概有7個參數(shù),想要合理使用線程池,肯定不會不會放過這些參數(shù)的優(yōu)化。
線程池參數(shù)
concurrent包最常用的就是線程池,平常工作建議直接使用線程池,Thread類就可以降低優(yōu)先級了。我們常用的主要有newSingleThreadExecutor、newFixedThreadPool、newCachedThreadPool、調(diào)度等,使用Executors工廠類創(chuàng)建。
newSingleThreadExecutor可以用于快速創(chuàng)建一個異步線程,非常方便。而newCachedThreadPool永遠(yuǎn)不要用在高并發(fā)的線上環(huán)境,它用的是無界隊列對任務(wù)進(jìn)行緩沖,可能會擠爆你的內(nèi)存。
我習(xí)慣性自定義ThreadPoolExecutor,也就是參數(shù)最全的那個。
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
假如我的任務(wù)可以預(yù)估,corePoolSize,maximumPoolSize一般都設(shè)成一樣大的,然后存活時間設(shè)的特別的長??梢员苊饩€程頻繁創(chuàng)建、關(guān)閉的開銷。I/O密集型和CPU密集型的應(yīng)用線程開的大小是不一樣的,一般I/O密集型的應(yīng)用線程就可以開的多一些。
threadFactory我一般也會定義一個,主要是給線程們起一個名字。這樣,在使用jstack等一些工具的時候,能夠直觀的看到我所創(chuàng)建的線程。
監(jiān)控
高并發(fā)下的線程池,最好能夠監(jiān)控起來??梢允褂萌罩?、存儲等方式保存下來,對后續(xù)的問題排查幫助很大。
通常,可以通過繼承ThreadPoolExecutor,覆蓋beforeExecute、afterExecute、terminated方法,達(dá)到對線程行為的控制和監(jiān)控。
線程池飽和策略
最容易被遺忘的可能就是線程的飽和策略了。也就是線程和緩沖隊列的空間全部用完了,新加入的任務(wù)將如何處置。jdk默認(rèn)實現(xiàn)了4種策略,默認(rèn)實現(xiàn)的是AbortPolicy,也就是直接拋出異常。下面介紹其他幾種。
DiscardPolicy 比abort更加激進(jìn),直接丟掉任務(wù),連異常信息都沒有。
CallerRunsPolicy 由調(diào)用的線程來處理這個任務(wù)。比如一個web應(yīng)用中,線程池資源占滿后,新進(jìn)的任務(wù)將會在tomcat線程中運行。這種方式能夠延緩部分任務(wù)的執(zhí)行壓力,但在更多情況下,會直接阻塞主線程的運行。
DiscardOldestPolicy 丟棄隊列最前面的任務(wù),然后重新嘗試執(zhí)行任務(wù)(重復(fù)此過程)。
很多情況下,這些飽和策略可能并不能滿足你的需求,你可以自定義自己的策略,比如將任務(wù)持久化到一些存儲中。
阻塞隊列
阻塞隊列會對當(dāng)前的線程進(jìn)行阻塞。當(dāng)隊列中有元素后,被阻塞的線程會自動被喚醒,這極大的提高的編碼的靈活性,非常方便。在并發(fā)編程中,一般推薦使用阻塞隊列,這樣實現(xiàn)可以盡量地避免程序出現(xiàn)意外的錯誤。阻塞隊列使用最經(jīng)典的場景就是socket數(shù)據(jù)的讀取、解析,讀數(shù)據(jù)的線程不斷將數(shù)據(jù)放入隊列,解析線程不斷從隊列取數(shù)據(jù)進(jìn)行處理。
ArrayBlockingQueue對訪問者的調(diào)用默認(rèn)是不公平的,我們可以通過設(shè)置構(gòu)造方法參數(shù)將其改成公平阻塞隊列。
LinkedBlockingQueue隊列的默認(rèn)最大長度為Integer.MAX_VALUE,這在用做線程池隊列的時候,會比較危險。
SynchronousQueue是一個不存儲元素的阻塞隊列。每一個put操作必須等待一個take操作,否則不能繼續(xù)添加元素。隊列本身不存儲任何元素,吞吐量非常高。對于提交的任務(wù),如果有空閑線程,則使用空閑線程來處理;否則新建一個線程來處理任務(wù)”。它更像是一個管道,在一些通訊框架中(比如rpc),通常用來快速處理某個請求,應(yīng)用較為廣泛。
DelayQueue是一個支持延時獲取元素的無界阻塞隊列。放入DelayQueue的對象需要實現(xiàn)Delayed接口,主要是提供一個延遲的時間,以及用于延遲隊列內(nèi)部比較排序。這種方式通常能夠比大多數(shù)非阻塞的while循環(huán)更加節(jié)省cpu資源。
另外還有PriorityBlockingQueue和LinkedTransferQueue等,根據(jù)字面意思就能猜測它的用途。在線程池的構(gòu)造參數(shù)中,我們使用的隊列,一定要注意其特性和邊界。比如,即使是最簡單的newFixedThreadPool,在某些場景下,也是不安全的,因為它使用了無界隊列。
CountDownLatch
假如有一堆接口A-Y,每個接口的耗時最大是200ms,最小是100ms。
我的一個服務(wù),需要提供一個接口Z,調(diào)用A-Y接口對結(jié)果進(jìn)行聚合。接口的調(diào)用沒有順序需求,接口Z如何在300ms內(nèi)返回這些數(shù)據(jù)?
此類問題典型的還有賽馬問題,只有通過并行計算才能完成問題。歸結(jié)起來可以分為兩類:
實現(xiàn)任務(wù)的并行性
開始執(zhí)行前等待n個線程完成任務(wù)
在concurrent包出現(xiàn)之前,需要手工的編寫這些同步過程,非常復(fù)雜。現(xiàn)在就可以使用CountDownLatch和CyclicBarrier進(jìn)行便捷的編碼。
CountDownLatch是通過一個計數(shù)器來實現(xiàn)的,計數(shù)器的初始值為線程的數(shù)量。每當(dāng)一個線程完成了自己的任務(wù)后,計數(shù)器的值就會減1。當(dāng)計數(shù)器值到達(dá)0時,它表示所有的線程已經(jīng)完成了任務(wù),然后在閉鎖上等待的線程就可以恢復(fù)執(zhí)行任務(wù)。
CyclicBarrier與其類似,可以實現(xiàn)同樣的功能。不過在日常的工作中,使用CountDownLatch會更頻繁一些。
信號量
Semaphore雖然有一些應(yīng)用場景,但大部分屬于炫技,在編碼中應(yīng)該盡量少用。
信號量可以實現(xiàn)限流的功能,但它只是常用限流方式的一種。其他兩種是漏桶算法、令牌桶算法。
hystrix的熔斷功能,也有使用信號量進(jìn)行資源的控制。
Lock && Condition
在Java中,對于Lock和Condition可以理解為對傳統(tǒng)的synchronized和wait/notify機(jī)制的替代。concurrent包中的許多阻塞隊列,就是使用Condition實現(xiàn)的。
但這些類和函數(shù)對于初中級碼農(nóng)來說,難以理解,容易產(chǎn)生bug,應(yīng)該在業(yè)務(wù)代碼中嚴(yán)格禁止。但在網(wǎng)絡(luò)編程、或者一些框架類工程中,這些功能是必須的,萬不可將這部分的工作隨便分配給某個小弟。
End
不管是wait、notify,還是同步關(guān)鍵字或者鎖,能不用就不用,因為它們會引發(fā)程序的復(fù)雜性。最好的方式,是直接使用concurrent包所提供的機(jī)制,來規(guī)避一些編碼方面的問題。
concurrent包中的CAS概念,在一定程度上算是無鎖的一種實現(xiàn)。更專業(yè)的有類似disruptor的無鎖隊列框架,但它依然是建立在CAS的編程模型上的。近些年,類似AKKA這樣的事件驅(qū)動模型正在走紅,但編程模型簡單,不代表實現(xiàn)簡單,背后的工作依然需要多線程去協(xié)調(diào)。
golang引入?yún)f(xié)程(coroutine)概念以后,對多線程加入了更加輕量級的補(bǔ)充。java中可以通過javaagent技術(shù)加載quasar補(bǔ)充一些功能,但我覺得你不會為了這丁點效率去犧牲編碼的可讀性。
感謝你能夠認(rèn)真閱讀完這篇文章,希望小編分享的“JAVA多線程的使用場景與注意事項有哪些”這篇文章對大家有幫助,同時也希望大家多多支持創(chuàng)新互聯(lián),關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道,更多相關(guān)知識等著你來學(xué)習(xí)!
當(dāng)前標(biāo)題:JAVA多線程的使用場景與注意事項有哪些
本文地址:http://jinyejixie.com/article20/ijcoco.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供手機(jī)網(wǎng)站建設(shè)、域名注冊、網(wǎng)站排名、App開發(fā)、網(wǎng)站改版、網(wǎng)站設(shè)計公司
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時需注明來源: 創(chuàng)新互聯(lián)