多線程
1、線程是程序運(yùn)行的最小單位,是操作系統(tǒng)調(diào)度的最小單位。
2、一個進(jìn)程包含多個線程,多個線程共享所屬進(jìn)程的資源(CPU、IO等)和內(nèi)存空間,但是每個線程有自己獨(dú)立的??臻g。
3、使用多線程可以提高CPU利用率,提高系統(tǒng)響應(yīng)速度。
4、降低資源消耗。通過重復(fù)利用已創(chuàng)建的線程降低線程創(chuàng)建和銷毀造成的消耗。
5、提高響應(yīng)速度。當(dāng)任務(wù)到達(dá)時,可以不需要等到線程創(chuàng)建就能立即執(zhí)行。
6、提高線程的可管理性。統(tǒng)一管理線程,避免系統(tǒng)創(chuàng)建大量同類線程而導(dǎo)致消耗完內(nèi)存。
進(jìn)程和線程的區(qū)別
1、系統(tǒng)會為進(jìn)程分配地址空間,不會為線程分配地址空間,但是線程有自己的堆棧和局部變量表。
2、系統(tǒng)會為進(jìn)程分配內(nèi)存,不會為線程分配內(nèi)存。
3、進(jìn)程和線程切換時都會切換上下文,但是進(jìn)程切換比線程切換更耗時,更耗資源。
4、線程相對進(jìn)程并發(fā)性較高。
5、進(jìn)程有自己的程序運(yùn)行入口可以獨(dú)立運(yùn)行,線程得依賴程序的調(diào)度運(yùn)行。
6、在保護(hù)模式下進(jìn)程崩潰之后不會對其它進(jìn)程產(chǎn)生影響,但是線程崩潰了所屬的進(jìn)程也會崩潰掉。
線程創(chuàng)建方式
1、繼承Thread類。
2、實(shí)現(xiàn)Runnable接口。
3、實(shí)現(xiàn)Callable接口(Future、FutureTask配合可以用來獲取異步執(zhí)行的結(jié)果, Callable 接口 call 方法允許拋出異常,可以獲取異常信息 注:Callalbe接口支持返回執(zhí)行結(jié)果,需要調(diào)用FutureTask.get()得到)。
線程狀態(tài)
線程有五種狀態(tài):1.準(zhǔn)備,新建線程,沒有執(zhí)行start方法。2.就緒,執(zhí)行start方法,初始化線程,準(zhǔn)備時間片。3.運(yùn)行,執(zhí)行run方法。4.阻塞。(造成線程阻塞原因:sleep,wait,使用阻塞IO,同步鎖,suspend懸掛)5.銷毀。
線程通信
主要包含兩種方式Monitor和Condition
1、notify/notifyAll
2、signal/signalAll
線程停止
1、使用退出標(biāo)志,使線程正常退出,也就是當(dāng)run方法完成后線程終止。
2、使用stop方法強(qiáng)行終止,但是不推薦這個方法,因?yàn)閟top和suspend及resume一樣都是過期作廢的方法。
3、使用interrupt方法中斷線程。調(diào)用interrupt方法,可以通過isInterrupt方法判斷線程終止。
線程共享數(shù)據(jù)
1、如果多個線程執(zhí)行同一個Runnable實(shí)現(xiàn)類中的代碼,此時共享的數(shù)據(jù)放在Runnable實(shí)現(xiàn)類中;
2、如果多個線程執(zhí)行不同的Runnable實(shí)現(xiàn)類中的代碼,此時共享數(shù)據(jù)和操作共享數(shù)據(jù)的方法封裝到一個對象中,在不同的Runnable實(shí)現(xiàn)類中調(diào)用操作共享數(shù)據(jù)的方法。
線程異常
如果異常沒有被捕獲該線程將會停止執(zhí)行。Thread.UncaughtExceptionHandler是用于處理未捕獲異常造成線程突然中斷情況的一個內(nèi)嵌接口。當(dāng)一個未捕獲異常將造成線程中斷的時候,JVM 會使用 Thread.getUncaughtExceptionHandler()來查詢線程的 UncaughtExceptionHandler 并將線程和異常作為參數(shù)傳遞給 handler 的 uncaughtException()方法進(jìn)行處理。
線程順序執(zhí)行
假設(shè)有T1、T2、T3三個線程,你怎樣保證T2在T1執(zhí)行完后執(zhí)行,T3在T2執(zhí)行完后執(zhí)行?可以使用join方法解決這個問題。比如在線程A中,調(diào)用線程B的join方法表示的意思就是:A等待B線程執(zhí)行完畢后(釋放CPU執(zhí)行權(quán)),在繼續(xù)執(zhí)行。
start()、run() 方法
1、new 一個 Thread,線程進(jìn)入了新建狀態(tài)。調(diào)用 start() 方法,會啟動一個線程并使線程進(jìn)入了就緒狀態(tài),當(dāng)分配到時間片后就可以開始運(yùn)行了。 start() 會執(zhí)行線程的相應(yīng)準(zhǔn)備工作,然后自動執(zhí)行 run() 方法的內(nèi)容,這是真正的多線程工作。
2、而直接執(zhí)行 run() 方法,會把 run 方法當(dāng)成一個 main 線程下的普通方法去執(zhí)行,并不會在某個線程中執(zhí)行它,所以這并不是多線程工作。
sleep()、 yield()方法
(1) sleep()方法給其他線程運(yùn)行機(jī)會時不考慮線程的優(yōu)先級,因此會給低優(yōu)先級的線程以運(yùn)行的機(jī)會;yield()方法只會給相同優(yōu)先級或更高優(yōu)先級的線程以運(yùn)行的機(jī)會;
(2) 線程執(zhí)行 sleep()方法后轉(zhuǎn)入阻塞(blocked)狀態(tài),而執(zhí)行 yield()方法后轉(zhuǎn)入就緒(ready)狀態(tài);
(3)sleep()方法聲明拋出 InterruptedException,而 yield()方法沒有聲明任何異常;
(4)sleep()方法比 yield()方法(跟操作系統(tǒng) CPU 調(diào)度相關(guān))具有更好的可移植性,通常不建議使用yield()方法來控制并發(fā)線程的執(zhí)行。
sleep和sleep(0)的區(qū)別
1、當(dāng) timeout = 0, 即 Sleep(0),如果線程調(diào)度器的可運(yùn)行隊(duì)列中有大于或等于當(dāng)前線程優(yōu)先級的就緒線程存在,操作系統(tǒng)會將當(dāng)前線程從處理器上移除,調(diào)度其他優(yōu)先級高的就緒線程運(yùn)行;如果可運(yùn)行隊(duì)列中的沒有就緒線程或所有就緒線程的優(yōu)先級均低于當(dāng)前線程優(yōu)先級,那么當(dāng)前線程會繼續(xù)執(zhí)行,就像沒有調(diào)用 Sleep(0)一樣。
2、當(dāng) timeout > 0 時,如:Sleep(1),會引發(fā)線程上下文切換:調(diào)用線程會從線程調(diào)度器的可運(yùn)行隊(duì)列中被移除一段時間,這個時間段約等于 timeout 所指定的時間長度。
線程等待、阻塞
線程等待是主動釋放CPU資源,等待某個條件滿足后再繼續(xù)執(zhí)行,而線程阻塞是被動等待某個條件的滿足,期間會一直占用CPU資源。
線程調(diào)度
1、分時調(diào)度:是指讓所有的線程輪流獲得 cpu 的使用權(quán),并且平均分配每個線程占用的 CPU 的時間片這個也比較好理解。
2、搶占式調(diào)度:Java虛擬機(jī)采用搶占式調(diào)度模型,是指優(yōu)先讓可運(yùn)行池中優(yōu)先級高的線程占用CPU,如果可運(yùn)行池中的線程優(yōu)先級相同,那么就隨機(jī)選擇一個線程,使其占用CPU。處于運(yùn)行狀態(tài)的線程會一直運(yùn)行,直至它不得不放棄 CPU。
JMM內(nèi)存模型
每個線程運(yùn)行時,都會創(chuàng)建一個工作內(nèi)存(也叫??臻g),來保存線程所有的私有變量。而JMM內(nèi)存模型規(guī)范中規(guī)定所有的變量都存儲在主內(nèi)存中,而主內(nèi)存中的變量是所有的線程都可以共享的,而對主內(nèi)存中的變量進(jìn)行操作時,必須在線程的工作內(nèi)存進(jìn)行操作,首先將主內(nèi)存的變量拷貝到工作內(nèi)存,進(jìn)行操作后,再將變量刷回到主內(nèi)存中。所有線程只有通過主內(nèi)存來進(jìn)行通信。
原子類
用過哪些原子類,他們的原理是什么?
1、Atomic基本原子類:AtomicInteger、AtomicLong、AtomicBoolean
2、AtomicArray數(shù)組類型原子類:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
3、AtomicReference引用類型原子類:AtomicReference、AtomicStampedReference、AtomicMarkableReference
4、AtomicFieldUpdater升級類型原子類:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater
5、Adder累加器:LongAdder、DoubleAdder
原子類實(shí)現(xiàn)原理: 原子類是通過自旋CAS操作volatile變量實(shí)現(xiàn)的。
volatile
1、變量可見性:JMM會把該線程本地內(nèi)存中的變量強(qiáng)制刷新到主內(nèi)存中去,每次讀取前必須先從主存刷新最新的值。
2、保證不了原子性:替代不了鎖。
3、禁止指令重排:避免指令并行運(yùn)行。
ThreadLocal
ThreadLocal叫做線程變量,意思是ThreadLocal中填充的變量屬于當(dāng)前線程,該變量對其他線程而言是隔離的,也就是說該變量是當(dāng)前線程獨(dú)有的變量。ThreadLocal為變量在每個線程中都創(chuàng)建了一個副本,那么每個線程可以訪問自己內(nèi)部的副本變量。
ThreadLocal使用場景
1、管理Session會話:將Session保存在ThreadLocal中,使線程處理多次會話時始終是同一個Session。
2、JDBC 連接 Connection,這樣就可以保證每個線程的都在各自的 Connection 上進(jìn)行數(shù)據(jù)庫的操作,不會出現(xiàn) A 線程關(guān)了 B線程正在使用的 Connection。
ThreadLocal與Synchronized的區(qū)別
ThreadLocal<T>其實(shí)是與線程綁定的一個變量。ThreadLocal和Synchonized都用于解決多線程并發(fā)訪問。
但是ThreadLocal與synchronized有本質(zhì)的區(qū)別:
1、Synchronized用于線程間的數(shù)據(jù)共享,而ThreadLocal則用于線程間的數(shù)據(jù)隔離。
2、Synchronized是利用鎖的機(jī)制,使變量或代碼塊在某一時該只能被一個線程訪問。而ThreadLocal為每一個線程都提供了變量的副本,使得每個線程在某一時間訪問到的并不是同一個對象,這樣就隔離了多個線程對數(shù)據(jù)的數(shù)據(jù)共享。
而Synchronized卻正好相反,它用于在多個線程間通信時能夠獲得數(shù)據(jù)共享。
ThreadLocal內(nèi)存泄露
每個線程都有?個ThreadLocalMap的內(nèi)部屬性,map的key是ThreaLocal,定義為弱引用,value是強(qiáng)引用類型。垃圾回收的時候會自動回收key,而value的回收取決于Thread對象的生命周期。一般會通過線程池的方式復(fù)用線程節(jié)省資源,這也就導(dǎo)致了線程對象的生命周期比較長,這樣便一直存在一條強(qiáng)引用鏈的關(guān)系:Thread --> ThreadLocalMap-->Entry-->Value,隨著任務(wù)的執(zhí)行,value就有可能越來越多且無法釋放,最終導(dǎo)致內(nèi)存泄漏。
ThreadLocal正確使用方法
1、每次使用完ThreadLocal都調(diào)用它的remove()方法清除數(shù)據(jù)
2、將ThreadLocal變量定義成private static,這樣就一直存在ThreadLocal的強(qiáng)引用,也就能保證任何時候都能通過ThreadLocal的弱引用訪問到Entry的value值,進(jìn)而清除掉 。
FutureTask
FutureTask 表示一個異步運(yùn)算的任務(wù)。FutureTask 里面可以傳入一個 Callable 的具體實(shí)現(xiàn)類,可以對這個異步運(yùn)算的任務(wù)的結(jié)果進(jìn)行等待獲取、判斷是否已經(jīng)完成、取消任務(wù)等操作。只有當(dāng)運(yùn)算完成的時候結(jié)果才能取回,如果運(yùn)算尚未完成 get 方法將會阻塞。一個 FutureTask 對象可以對調(diào)用了 Callable 和 Runnable 的對象進(jìn)行包裝,由于 FutureTask 也是Runnable 接口的實(shí)現(xiàn)類,所以 FutureTask 也可以放入線程池中。
synchronized、Lock
1、synchronized是Java關(guān)鍵字,在JVM層面實(shí)現(xiàn)加鎖和解鎖;Lock是一個接口,在代碼層面實(shí)現(xiàn)加鎖和解鎖。
2、synchronized可以用在代碼塊上、方法上;Lock只能寫在代碼里。
3、synchronized在代碼執(zhí)行完或出現(xiàn)異常時自動釋放鎖;Lock不會自動釋放鎖,需要在finally中顯示釋放鎖。
4、synchronized會導(dǎo)致線程拿不到鎖一直等待;Lock可以設(shè)置獲取鎖失敗的超時時間。
5、synchronized無法得知是否獲取鎖成功;Lock則可以通過tryLock得知加鎖是否成功。
6、synchronized鎖可重入、不可中斷、非公平;Lock鎖可重入、可中斷、可公平/不公平,并可以細(xì)分讀寫鎖以提高效率。
1、公平性:lock支持公平鎖,sync不支持公平鎖
2、鎖狀態(tài):lock可以獲取鎖狀態(tài),sync無法獲取鎖狀態(tài)
3、性能:資源競爭激烈時lock性能優(yōu)異,資源競爭不激烈時sync性能優(yōu)異。
synchronized實(shí)現(xiàn)原理
1、synchronized作用在代碼塊時,它的底層是通過monitorenter、monitorexit指令來實(shí)現(xiàn)的。
monitorenter:每個對象都是一個監(jiān)視器鎖(monitor),當(dāng)monitor被占用時就會處于鎖定狀態(tài),線程執(zhí)行monitorenter指令時嘗試獲取monitor的所有權(quán),過程如下:如果monitor的進(jìn)入數(shù)為0,則該線程進(jìn)入monitor,然后將進(jìn)入數(shù)設(shè)置為1,該線程即為monitor的所有者。如果線程已經(jīng)占有該monitor,只是重新進(jìn)入,則進(jìn)入monitor的進(jìn)入數(shù)加1。如果其他線程已經(jīng)占用了monitor,則該線程進(jìn)入阻塞狀態(tài),直到monitor的進(jìn)入數(shù)為0,再重新嘗試獲取monitor的所有權(quán)。
monitorexit:執(zhí)行monitorexit的線程必須是objectref所對應(yīng)的monitor持有者。指令執(zhí)行時,monitor的進(jìn)入數(shù)減1,如果減1后進(jìn)入數(shù)為0,那線程退出monitor,不再是這個monitor的所有者。其他被這個monitor阻塞的線程可以嘗試去獲取這個monitor的所有權(quán)。
2、方法的同步并沒有通過 monitorenter 和 monitorexit 指令來完成,不過相對于普通方法,其常量池中多了 ACC_SYNCHRONIZED 標(biāo)示符。JVM就是根據(jù)該標(biāo)示符來實(shí)現(xiàn)方法的同步的:當(dāng)方法調(diào)用時,調(diào)用指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標(biāo)志是否被設(shè)置,如果設(shè)置了,執(zhí)行線程將先獲取monitor,獲取成功之后才能執(zhí)行方法體,方法執(zhí)行完后再釋放monitor。在方法執(zhí)行期間,其他任何線程都無法再獲得同一個monitor對象。
synchronized和CAS的區(qū)別?什么場景使用CAS?什么場景使用synchronized
簡單的來說CAS適用于寫比較少的情況下(多讀場景,沖突一般較少)
synchronized適用于寫比較多的情況下(多寫場景,沖突一般較多)
1.對于資源競爭較少(線程沖突較輕)的情況,使用synchronized同步鎖進(jìn)行線程阻塞和喚醒切換以及用戶態(tài)內(nèi)核態(tài)間的切換操作額外浪費(fèi)消耗cpu資源;而CAS基于硬件實(shí)現(xiàn),不需要進(jìn)入內(nèi)核,不需要切換線程,操作自旋幾率較少,因此可以獲得更高的性能。
2.對于資源競爭嚴(yán)重(線程沖突嚴(yán)重)的情況,CAS自旋的概率會比較大,從而浪費(fèi)更多的CPU資源,效率低于synchronized。
synchronized是如何實(shí)現(xiàn)可重入性的
synchronized關(guān)鍵字經(jīng)過編譯后,會在同步塊的前后分別形成monitorenter和monitorexit兩個字節(jié)碼指令。每個鎖對象內(nèi)部維護(hù)一個計(jì)數(shù)器,該計(jì)數(shù)器初始值為0,表示任何線程都可以獲取該鎖并執(zhí)行相應(yīng)的方法。根據(jù)虛擬機(jī)規(guī)范要求,在執(zhí)行monitorenter指令時,首先要嘗試獲取對象的鎖,如果這個對象沒有被鎖定,或者當(dāng)前線程已經(jīng)擁有了對象的鎖,把鎖的計(jì)數(shù)器+1,相應(yīng)的在執(zhí)行monitorexit指令后鎖計(jì)數(shù)器-1,當(dāng)計(jì)數(shù)器為0時,鎖就被釋放。如果獲取對象鎖失敗,那當(dāng)前線程就要阻塞等待,直到對象鎖被另一個線程釋放為止。
使用synchronized修飾靜態(tài)方法和非靜態(tài)方法的區(qū)別
1、Synchronized修飾非靜態(tài)方法,實(shí)際上是對調(diào)用該方法的對象加鎖,俗稱“對象鎖”。
<1> 同一個對象在兩個線程中分別訪問該對象的兩個同步方法,會產(chǎn)生互斥。
<2> 不同對象在兩個線程中調(diào)用同一個同步方法,不會產(chǎn)生互斥。
2、Synchronized修飾靜態(tài)方法,實(shí)際上是對該類對象加鎖,俗稱“類鎖”。
<1> 用類直接在兩個線程中調(diào)用兩個不同的同步方法,會產(chǎn)生互斥。
<2> 用一個類的靜態(tài)對象在兩個線程中調(diào)用靜態(tài)方法或非靜態(tài)方法,會產(chǎn)生互斥。
<3> 一個對象在兩個線程中分別調(diào)用一個靜態(tài)同步方法和一個非靜態(tài)同步方法,不會產(chǎn)生互斥。
synchronized加在this和class區(qū)別
synchronized 加鎖 class 時,無論共享一個對象還是創(chuàng)建多個對象,它們用的都是同一把鎖,而使用 synchronized 加鎖 this 時,只有同一個對象會使用同一把鎖,不同對象之間的鎖是不同的。
線程安全集合
1、HashTable/Vector(性能較差已經(jīng)棄用)
2、Collections.synchroniedXXX方法包裝的集合。
3、JUC下面的以Concurrent/CopyOnWrite開頭的集合
4、BlockingQueue: BlockingQueue是阻塞隊(duì)列,內(nèi)部維護(hù)了一個RetrantLock和2個Condition來實(shí)現(xiàn)生產(chǎn)者和消費(fèi)者線程同步。當(dāng)隊(duì)列為空時消費(fèi)者線程阻塞等待生產(chǎn)者線程往隊(duì)列中添加元素,當(dāng)隊(duì)列滿時生產(chǎn)者線程阻塞等待消費(fèi)者線程從隊(duì)列中取元素。
<1> DelayQueue:基于時間優(yōu)先級的隊(duì)列,延期阻塞隊(duì)列
<2> ArrayBlockingQueue:基于數(shù)組的并發(fā)阻塞隊(duì)列
<3> LinkedBlockingQueue:基于鏈表的FIFO阻塞隊(duì)列
<4> PriorityBlockingQueue:帶優(yōu)先級的無界阻塞隊(duì)列
<5> SynchronousQueue:并發(fā)同步阻塞隊(duì)列
AQS
抽象隊(duì)列同步器AbstractQueuedSynchronizer ,為構(gòu)建鎖或者其他同步組件提供了一個框架,AQS采用模板方法模式,子類只需要實(shí)現(xiàn)特定的幾個方法來實(shí)現(xiàn)鎖或者同步邏輯。AQS內(nèi)部維護(hù)了一個int成員變量來表示同步狀態(tài),通過內(nèi)置的FIFO(first-in-first-out)同步隊(duì)列來控制獲取共享資源的線程?;贏QS實(shí)現(xiàn)的有RetrantLock、Semaphore、CyclicBarria,CountDownLatch。
四種同步器
1、CountDownLatch:就是等待其他線程執(zhí)行完任務(wù),并且必要時可以對各個任務(wù)的結(jié)果進(jìn)行匯總,然后主線程繼續(xù)往下執(zhí)行;
2、CyclicBarrier:可以讓一組線程達(dá)到一個屏障時被阻塞,直到最后一個線程到達(dá)屏障時,所有線程才可以繼續(xù)執(zhí)行;根據(jù)字面意思就是回環(huán)。
3、信號量:信號量核心作用是控制并發(fā)執(zhí)行的線程數(shù)量。定義信號量的時候會初始化一個預(yù)設(shè)值n,即最多有n個線程同時執(zhí)行,如果將n設(shè)為1,就達(dá)到了互斥鎖的效果。
AQS加解鎖流程
1、加鎖:先通過tryAcquire來嘗試獲取鎖,如果獲取成功就可以執(zhí)行你的代碼邏輯了。如果獲取失敗,那么就創(chuàng)建一個 Node 節(jié)點(diǎn),并把當(dāng)前 線程 設(shè)置到Node節(jié)點(diǎn)中,最后把Node節(jié)點(diǎn)添加到內(nèi)部維護(hù)的一個隊(duì)列尾部,并阻塞住Node對應(yīng)線程運(yùn)行。
2、解鎖:AQS內(nèi)部會從隊(duì)列頭部開始獲取下一個節(jié)點(diǎn),然后解除節(jié)點(diǎn)對應(yīng)的線程阻塞狀態(tài),然后把該節(jié)點(diǎn)設(shè)置成隊(duì)列的頭節(jié)點(diǎn)(這樣實(shí)現(xiàn)隊(duì)列頭節(jié)點(diǎn)出棧),最后該節(jié)點(diǎn)線程對應(yīng)的代碼邏輯得以繼續(xù)執(zhí)行。后面如此循環(huán),AQS隊(duì)列中所有節(jié)點(diǎn)對應(yīng)線程中鎖住的代碼塊得以順序執(zhí)行。
JUC
JUC是java提供的并發(fā)操作實(shí)現(xiàn)線程安全的包,主要包含以下類:
1、原子類
2、Lock鎖
3、線程池(ThreadPoolExecutor)
4、并發(fā)容器(Concurrent開頭的容器、CopyOnWrite開頭的容器、BlockingQueue)
5、同步器(CountDownLatch(允許一個或者多個線程等待其它線程操作完成)、信號量(控制同時訪問資源的數(shù)量)、CyclicBarrier(控制一組線程達(dá)到一個屏障時被阻塞,先到達(dá)的線程等待后到達(dá)的線程,當(dāng)所有的線程都到達(dá)時才會繼續(xù)執(zhí)行))
線程池
常見的幾種線程池
1、newCachedThreadPool():創(chuàng)建一個具有緩存功能的線程池,系統(tǒng)根據(jù)需要創(chuàng)建線程,這些線程將會被緩存在線程池中。
2、newFixedThreadPool(int nThreads):創(chuàng)建一個可重用的、具有固定線程數(shù)的線程池。
3、newSingleThreadExecutor():創(chuàng)建一個只有單線程的線程池,它相當(dāng)于調(diào)用newFixedThread Pool()方法時傳入?yún)?shù)為1。
4、newScheduledThreadPool(int corePoolSize):創(chuàng)建具有指定線程數(shù)的線程池,它可以在指定延遲后執(zhí)行線程任務(wù)。
5、corePoolSize指池中所保存的線程數(shù),即使線程是空閑的也被保存在線程池內(nèi)。
6、newSingleThreadScheduledExecutor():創(chuàng)建只有一個線程的線程池,它可以在指定延遲后執(zhí)行線程任務(wù)。
線程池的工作流程
1、判斷核心線程池是否已滿,沒滿則創(chuàng)建一個新的工作線程來執(zhí)行任務(wù)。
2、判斷任務(wù)隊(duì)列是否已滿,沒滿則將新提交的任務(wù)添加在工作隊(duì)列。
3、判斷整個線程池是否已滿,沒滿則創(chuàng)建一個新的工作線程來執(zhí)行任務(wù),已滿則執(zhí)行飽和(拒絕)策略。
線程池的拒絕策略
1、AbortPolicy:丟棄任務(wù)并拋出RejectedExecutionException異常。
2、DiscardPolicy:也是丟棄任務(wù),但是不拋出異常。
3、DiscardOldestPolicy:丟棄隊(duì)列最前面的任務(wù),然后重新嘗試執(zhí)行任務(wù)(重復(fù)該過程)。
4、CallerRunsPolicy:由調(diào)用線程處理該任務(wù)。
線程池參數(shù)
1、corePoolSize(核心工作線程數(shù)):當(dāng)向線程池提交一個任務(wù)時,若線程池已創(chuàng)建的線程數(shù)小于corePoolSize,即便此時存在空閑線程,也會通過創(chuàng)建一個新線程來執(zhí)行該任務(wù),直到已創(chuàng)建的線程數(shù)大于或等于corePoolSize時。
2、maximumPoolSize(最大線程數(shù)):線程池所允許的最大線程個數(shù)。當(dāng)隊(duì)列滿了,且已創(chuàng)建的線程數(shù)小于maximumPoolSize,則線程池會創(chuàng)建新的線程來執(zhí)行任務(wù)。另外,對于無界隊(duì)列,可忽略該參數(shù)。
3、keepAliveTime(多余線程存活時間):當(dāng)線程池中線程數(shù)大于核心線程數(shù)時,線程的空閑時間如果超過線程存活時間,那么這個線程就會被銷毀,直到線程池中的線程數(shù)小于等于核心線程數(shù)。
4、workQueue(隊(duì)列):用于傳輸和保存等待執(zhí)行任務(wù)的阻塞隊(duì)列。
5、threadFactory(線程創(chuàng)建工廠):用于創(chuàng)建新線程。threadFactory創(chuàng)建的線程也是采用new Thread()方式,threadFactory創(chuàng)建的線程名都具有統(tǒng)一的風(fēng)格:pool-m-thread-n(m為線程池的編號,n為線程池內(nèi)的線程編號)。
6、handler(拒絕策略):當(dāng)線程池和隊(duì)列都滿了,再加入線程會執(zhí)行此策略
線程池的生命周期
1、Running:線程池正在運(yùn)行中。
2、SHUTDOWN:線程池中不再接收新任務(wù),等待隊(duì)列中所有任務(wù)執(zhí)行完畢。
3、STOP:立馬停止,清空隊(duì)列中的所有任務(wù),已有任務(wù)也不再執(zhí)行。
4、TIDING:線程池中隊(duì)列為空。
5、TERMINATE:線程池死亡。
線程池中的線程是怎么創(chuàng)建的?是一開始就隨著線程池的啟動創(chuàng)建好的嗎?
每當(dāng)我們調(diào)用execute()方法添加一個任務(wù)時,線程池會做如下判斷:·如果正在運(yùn)行的線程數(shù)量小于corePoolSize,那么馬上創(chuàng)建線程運(yùn)行這個任務(wù);如果正在運(yùn)行的線程數(shù)量大于或等于corePoolSize,那么將這個任務(wù)放入隊(duì)列;如果這時候隊(duì)列滿了,而且正在運(yùn)行的線程數(shù)量小于maximumPoolSize,那么還是要創(chuàng)建非核心線程立刻運(yùn)行這個任務(wù);·如果隊(duì)列滿了,而且正在運(yùn)行的線程數(shù)量大于或等于maximumPoolSize,那么線程池會拋出異常RejectExecutionException。當(dāng)一個線程完成任務(wù)時,它會從隊(duì)列中取下一個任務(wù)來執(zhí)行。當(dāng)一個線程無事可做,超過一定的時間(keepAliveTime)時,線程池會判斷。
如果當(dāng)前運(yùn)行的線程數(shù)大于corePoolSize,那么這個線程就被停掉。所以線程池的所有任務(wù)完成后,它最終會收縮到corePoolSize的大小。
線程池中阻塞隊(duì)列的長度如何設(shè)置
要結(jié)合具體的業(yè)務(wù)場景要求,如果系統(tǒng)要求相應(yīng)塊的話,可以將隊(duì)列大小設(shè)置小一點(diǎn),比如0,如果系統(tǒng)要求沒那么快的話,可以設(shè)置大一點(diǎn)。建議采用tomcat的處理方式,core與max一致,先擴(kuò)容到max再放隊(duì)列,不過隊(duì)列長度要根據(jù)使用場景設(shè)置一個上限值,如果響應(yīng)時間要求較高的系統(tǒng)可以設(shè)置為0。
線程池中submit()和execute() 方法有什么區(qū)別
1、相同點(diǎn):都可以開啟線程執(zhí)行池中的任務(wù)。
2、不同點(diǎn):
接收參數(shù):execute()只能執(zhí)行 Runnable 類型的任務(wù)。submit()可以執(zhí)行 Runnable 和 Callable 類型的任務(wù)。
返回值:submit()方法可以返回持有計(jì)算結(jié)果的 Future 對象,而execute()沒有
異常處理:submit()方便Exception處理
提交任務(wù)時線程池隊(duì)列已滿
有倆種可能:
1、如果使用的是無界隊(duì)列 LinkedBlockingQueue,也就是無界隊(duì)列的話,沒關(guān)系,繼續(xù)添加任務(wù)到阻塞隊(duì)列中等待執(zhí)行,因?yàn)?LinkedBlockingQueue 可以近乎認(rèn)為是一個無窮大的隊(duì)列,可以無限存放任務(wù)。
2、如果使用的是有界隊(duì)列比如 ArrayBlockingQueue,任務(wù)首先會被添加到ArrayBlockingQueue 中,ArrayBlockingQueue 滿了,會根據(jù)maximumPoolSize 的值增加線程數(shù)量,如果增加了線程數(shù)量還是處理不過來,ArrayBlockingQueue 繼續(xù)滿,那么則會使用拒絕策略RejectedExecutionHandler 處理滿了的任務(wù),默認(rèn)是 AbortPolicy。
線程池的關(guān)閉方式
1、shutdownNow:會首先將線程池的狀態(tài)設(shè)置成STOP,然后嘗試停止所有的正在執(zhí)行或暫停任務(wù)的線程。
2、shutdown:將線程池的狀態(tài)設(shè)置成SHUTDOWN狀態(tài),然后中斷所有沒有正在執(zhí)行任務(wù)的線程。
線程池的任務(wù)執(zhí)行完成判斷
1、使用線程池的原生函數(shù)isTerminated();executor提供一個原生函數(shù)isTerminated()來判斷線程池中的任務(wù)是否全部完成。如果全部完成返回true,否則返回false。
2、使用重入鎖,維持一個公共計(jì)數(shù)。所有的普通任務(wù)維持一個計(jì)數(shù)器,當(dāng)任務(wù)完成時計(jì)數(shù)器加一(這里要加鎖),當(dāng)計(jì)數(shù)器的值等于任務(wù)數(shù)時,這時所有的任務(wù)已經(jīng)執(zhí)行完畢了。
3、使用CountDownLatch。它的原理跟第二種方法類似,給CountDownLatch一個計(jì)數(shù)值,任務(wù)執(zhí)行完畢后,調(diào)用countDown()執(zhí)行計(jì)數(shù)值減一。最后執(zhí)行的任務(wù)在調(diào)用方法的開始調(diào)用await()方法,這樣整個任務(wù)會阻塞,直到這個計(jì)數(shù)值為零,才會繼續(xù)執(zhí)行。這種方式的缺點(diǎn)就是需要提前知道任務(wù)的數(shù)量。
4、submit向線程池提交任務(wù),使用Future判斷任務(wù)執(zhí)行狀態(tài)。使用submit向線程池提交任務(wù)與execute提交不同,submit會有Future類型的返回值。通過future.isDone()方法可以知道任務(wù)是否執(zhí)行完成。
線程池阻塞隊(duì)列選擇
1、ArrayBlockingQueue(常用):都是共用同一個鎖對象,由此也意味著兩者無法真正并行運(yùn)行。
2、LinkedBlockingQueue(常用):生產(chǎn)者端和消費(fèi)者端分別采用了獨(dú)立的鎖來控制數(shù)據(jù)同步,這也意味著在高并發(fā)
的情況下生產(chǎn)者和消費(fèi)者可以并行地操作隊(duì)列中的數(shù)據(jù),以此來提高整個隊(duì)列的并發(fā)性能。
3、DelayQueue:DelayQueue 中的元素只有當(dāng)其指定的延遲時間到了,才能夠從隊(duì)列中獲取到該元素。DelayQueue 是一個沒有大小限制的隊(duì)列,因此往隊(duì)列中插入數(shù)據(jù)的操作(生產(chǎn)者)永遠(yuǎn)不會被阻塞,而只有獲取數(shù)據(jù)的操作(消費(fèi)者)才會被阻
塞。
4、PriorityBlockingQueue:不會阻塞數(shù)據(jù)生產(chǎn)者,而只會在沒有可消費(fèi)的數(shù)據(jù)時,阻塞數(shù)據(jù)的消費(fèi)者。
因此使用的時候要特別注意,生產(chǎn)者生產(chǎn)數(shù)據(jù)的速度絕對不能快于消費(fèi)者消費(fèi)數(shù)據(jù)的速度,否則時間一長,會最終耗盡所有的可用堆內(nèi)存空間。
5、SynchronousQueue:不存儲元素的阻塞隊(duì)列,也即單個元素的隊(duì)列。
6、LinkedTransferQueue:由鏈表組成的無界阻塞隊(duì)列。
7、LinkedBlockingDeque:由鏈表組成的雙向阻塞隊(duì)列。
鎖
ReentrantLock實(shí)現(xiàn)原理
ReentrantLock是基于AQS實(shí)現(xiàn)的,AQS即AbstractQueuedSynchronizer的縮寫,這個是個內(nèi)部實(shí)現(xiàn)了兩個隊(duì)列的抽象類,分別是同步隊(duì)列和條件隊(duì)列。其中同步隊(duì)列是一個雙向鏈表,里面儲存的是處于等待狀態(tài)的線程,正在排隊(duì)等待喚醒去獲取鎖,而條件隊(duì)列是一個單向鏈表,里面儲存的也是處于等待狀態(tài)的線程,只不過這些線程喚醒的結(jié)果是加入到了同步隊(duì)列的隊(duì)尾,AQS所做的就是管理這兩個隊(duì)列里面線程之間的等待狀態(tài)-喚醒的工作。
在同步隊(duì)列中,還存在2中模式,分別是獨(dú)占模式和共享模式,這兩種模式的區(qū)別就在于AQS在喚醒線程節(jié)點(diǎn)的時候是不是傳遞喚醒,這兩種模式分別對應(yīng)獨(dú)占鎖和共享鎖。
AQS是一個抽象類,所以不能直接實(shí)例化,當(dāng)我們需要實(shí)現(xiàn)一個自定義鎖的時候可以去繼承AQS然后重寫獲取鎖的方式和釋放鎖的方式還有管理state,而ReentrantLock就是通過重寫了AQS的tryAcquire和tryRelease方法實(shí)現(xiàn)的lock和unlock。
ReentrantLock是如何實(shí)現(xiàn)可重入性的
ReentrantLock使用內(nèi)部類Sync來管理鎖,所以真正的獲取鎖是由Sync的實(shí)現(xiàn)類控制的。Sync有兩個實(shí)現(xiàn),分別為NonfairSync(非公公平鎖)和FairSync(公平鎖)。Sync通過繼承AQS實(shí)現(xiàn),在AQS中維護(hù)了一個private volatile int state來計(jì)算重入次數(shù),避免頻繁的持有釋放操作帶來的線程問題。
自旋鎖
自旋鎖的定義:當(dāng)一個線程嘗試去獲取某一把鎖的時候,如果這個鎖此時已經(jīng)被別人獲取(占用),那么此線程就無法獲取到這把鎖,該線程將會等待,間隔一段時間后會再次嘗試獲取。這種采用循環(huán)加鎖 -> 等待的機(jī)制被稱為自旋鎖
自旋鎖的原理比較簡單,如果持有鎖的線程能在短時間內(nèi)釋放鎖資源,那么那些等待競爭鎖的線程就不需要做內(nèi)核態(tài)和用戶態(tài)之間的切換進(jìn)入阻塞狀態(tài),它們只需要等一等(自旋),等到持有鎖的線程釋放鎖之后即可獲取,這樣就避免了用戶進(jìn)程和內(nèi)核切換的消耗
讀寫鎖
與傳統(tǒng)鎖不同的是讀寫鎖的規(guī)則是可以共享讀,但只能一個寫,總結(jié)起來為:讀讀不互斥、讀寫互斥、寫寫互斥,而一般的獨(dú)占鎖是:讀讀互斥、讀寫互斥、寫寫互斥,而場景中往往讀遠(yuǎn)遠(yuǎn)大于寫,讀寫鎖就是為了這種優(yōu)化而創(chuàng)建出來的一種機(jī)制。 注意是讀遠(yuǎn)遠(yuǎn)大于寫,一般情況下獨(dú)占鎖的效率低來源于高并發(fā)下對臨界區(qū)的激烈競爭導(dǎo)致線程上下文切換。因此當(dāng)并發(fā)不是很高的情況下,讀寫鎖由于需要額外維護(hù)讀鎖的狀態(tài),可能還不如獨(dú)占鎖的效率高。因此需要根據(jù)實(shí)際情況選擇使用。
在Java中ReadWriteLock的主要實(shí)現(xiàn)為ReentrantReadWriteLock,其提供了以下特性:
公平性選擇:支持公平與非公平(默認(rèn))的鎖獲取方式,吞吐量非公平優(yōu)先于公平。
可重入:讀線程獲取讀鎖之后可以再次獲取讀鎖,寫線程獲取寫鎖之后可以再次獲取寫鎖。
可降級:寫線程獲取寫鎖之后,其還可以再次獲取讀鎖,然后釋放掉寫鎖,那么此時該線程是讀鎖狀態(tài),也就是降級操作。
分段鎖
容器里有多把鎖,每一把鎖用于鎖容器其中一部分?jǐn)?shù)據(jù),那么當(dāng)多線程訪問容器里不同數(shù)據(jù)段的數(shù)據(jù)時,線程間就不會存在鎖競爭,從而可以有效的提高并發(fā)訪問效率。這就是ConcurrentHashMap所使用的鎖分段技術(shù),首先將數(shù)據(jù)分成一段一段的存儲,然后給每一段數(shù)據(jù)配一把鎖,當(dāng)一個線程占用鎖訪問其中一個段數(shù)據(jù)的時候,其他段的數(shù)據(jù)也能被其他線程訪問。
樂觀鎖和悲觀鎖
1、悲觀鎖:總是假設(shè)最壞的情況,每次去拿數(shù)據(jù)的時候都認(rèn)為別人會修改,所以每次在拿數(shù)據(jù)的時候都會上鎖,這樣別人想拿這個數(shù)據(jù)就會阻塞直到它拿到鎖。Java中悲觀鎖是通過synchronized關(guān)鍵字或Lock接口來實(shí)現(xiàn)的。
2、樂觀鎖:顧名思義,就是很樂觀,每次去拿數(shù)據(jù)的時候都認(rèn)為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數(shù)據(jù)。樂觀鎖適用于多讀的應(yīng)用類型,這樣可以提高吞吐量。在JDK1.5 中新增 java.util.concurrent (J.U.C)就是建立在CAS之上的。相對于對于 synchronized 這種阻塞算法,CAS是非阻塞算法的一種常見實(shí)現(xiàn)。所以J.U.C在性能上有了很大的提升。
偏向鎖
顧名思義,它會偏向于第一個訪問鎖的線程,如果在運(yùn)行過程中,同步鎖只有一個線程訪問,不存在多線程爭用的情況,則線程是不需要觸發(fā)同步的,減少加鎖/解鎖的一些CAS操作(比如等待隊(duì)列的一些CAS操作),這種情況下,就會給線程加一個偏向鎖。 如果在運(yùn)行過程中,遇到了其他線程搶占鎖,則持有偏向鎖的線程會被掛起,JVM會消除它身上的偏向鎖,將鎖恢復(fù)到標(biāo)準(zhǔn)的輕量級鎖。
公平鎖與非公平鎖
1、公平鎖:每個線程獲取鎖的順序是按照線程訪問鎖的先后順序獲取的,最前面的線程總是最先獲取到鎖。在java中RetarntLock可以設(shè)置為公平鎖。實(shí)現(xiàn)原理:先將線程自己添加到等待隊(duì)列的隊(duì)尾并休眠,當(dāng)某線程用完鎖之后,會去喚醒等待隊(duì)列中隊(duì)首的線程嘗試去獲取鎖,鎖的使用順序也就是隊(duì)列中的先后順序,在整個過程中,線程會從運(yùn)行狀態(tài)切換到休眠狀態(tài),再從休眠狀態(tài)恢復(fù)成運(yùn)行狀態(tài),但線程每次休眠和恢復(fù)都需要從用戶態(tài)轉(zhuǎn)換成內(nèi)核態(tài),而這個狀態(tài)的轉(zhuǎn)換是比較慢的,所以公平鎖的執(zhí)行速度會比較慢。
2、非公平鎖:每個線程獲取鎖的順序是隨機(jī)的,并不會遵循先來先得的規(guī)則,所有線程會競爭獲取鎖。在Java中synchronized和RetrantLock可以設(shè)置為非公平鎖。實(shí)現(xiàn)原理:當(dāng)線程獲取鎖時,會先通過 CAS 嘗試獲取鎖,如果獲取成功就直接擁有鎖,如果獲取鎖失敗才會進(jìn)入等待隊(duì)列,等待下次嘗試獲取鎖。這樣做的好處是,獲取鎖不用遵循先到先得的規(guī)則,從而避免了線程休眠和恢復(fù)的操作,這樣就加速了程序的執(zhí)行效率。
共享式與獨(dú)占式鎖
同一時刻獨(dú)占式只能有一個線程獲取同步狀態(tài),而共享式在同一時刻可以有多個線程獲取同步狀態(tài)。例如讀操作可以有多個線程同時進(jìn)行,而寫操作同一時刻只能有一個線程進(jìn)行寫操作,其他操作都會被阻塞。
synchronized 鎖升級
1、synchronized 鎖升級原理:在鎖對象的對象頭里面有一個 threadid 字段,在第一次訪問的時候 threadid 為空,jvm 讓其持有偏向鎖,并將 threadid 設(shè)置為其線程 id,再次進(jìn)入的時候會先判斷 threadid 是否與其線程 id 一致,如果一致則可以直接使用此對象,如果不一致,則升級偏向鎖為輕量級鎖,通過自旋循環(huán)一定次數(shù)來獲取鎖,執(zhí)行一定次數(shù)之后,如果還沒有正常獲取到要使用的對象,此時就會把鎖從輕量級升級為重量級鎖,此過程就構(gòu)成了 synchronized 鎖的升級。
2、鎖的升級的目的:鎖升級是為了減低了鎖帶來的性能消耗。在 Java 6 之后優(yōu)化 synchronized 的實(shí)現(xiàn)方式,使用了偏向鎖升級為輕量級鎖再升級到重量級鎖的方式,從而減低了鎖帶來的性能消耗。
死鎖
1、定義:多個進(jìn)程或者線程在競爭資源時產(chǎn)生了資源依賴而導(dǎo)致互相等待的現(xiàn)象,如果沒有外力介入,就會一直等待下去無法繼續(xù)運(yùn)行。
2、條件:<1>資源在一段時間只能被一個進(jìn)程占據(jù) <2>當(dāng)某個資源被某個進(jìn)程占用之后其它進(jìn)程只能等待 <3>某個資源被占用之后,只能等待該進(jìn)程釋放 <4>循環(huán)等待
如何避免線程死鎖
1、避免一個線程同時獲得多個鎖
2、避免一個線程在鎖內(nèi)同時占用多個資源,盡量保證每個鎖只占用一個資源
3、嘗試使用定時鎖,使用lock.tryLock(timeout)來替代使用內(nèi)部鎖機(jī)制
CAS
1、CAS 是 compare and swap 的縮寫,即我們所說的比較交換。
2、cas 是一種基于鎖的操作,而且是樂觀鎖。在 java 中鎖分為樂觀鎖和悲觀鎖。悲觀鎖是將資源鎖住,等一個之前獲得鎖的線程釋放鎖之后,下一個線程才可以訪問。而樂觀鎖采取了一種寬泛的態(tài)度,通過某種方式不加鎖來處理資源,比如通過給記錄加 version 來獲取數(shù)據(jù),性能較悲觀鎖有很大的提高。
3、CAS 操作包含三個操作數(shù) —— 內(nèi)存位置(V)、預(yù)期原值(A)和新值(B)。如果內(nèi)存地址里面的值和 A 的值是一樣的,那么就將內(nèi)存里面的值更新成 B。CAS是通過無限循環(huán)來獲取數(shù)據(jù)的,若果在第一輪循環(huán)中,a 線程獲取地址里面的值被b 線程修改了,那么 a 線程需要自旋,到下次循環(huán)才有可能機(jī)會執(zhí)行。
CAS問題
1、ABA 問題:
比如說一個線程 one 從內(nèi)存位置 V 中取出 A,這時候另一個線程 two 也從內(nèi)存中取出 A,并且 two 進(jìn)行了一些操作變成了 B,然后 two 又將 V 位置的數(shù)據(jù)變成 A,這時候線程 one 進(jìn)行 CAS 操作發(fā)現(xiàn)內(nèi)存中仍然是 A,然后 one 操作成功。盡管線程 one 的 CAS 操作成功,但可能存在潛藏的問題。從 Java1.5 開始 JDK 的 atomic包里提供了一個類 AtomicStampedReference 來解決 ABA 問題。
2、循環(huán)時間長開銷大:對于資源競爭嚴(yán)重(線程沖突嚴(yán)重)的情況,CAS 自旋的概率會比較大,從而浪費(fèi)更多的 CPU 資源,效率低于 synchronized。
3、只能保證一個共享變量的原子操作:當(dāng)對一個共享變量執(zhí)行操作時,我們可以使用循環(huán) CAS 的方式來保證原子操作,但是對多個共享變量操作時,循環(huán) CAS 就無法保證操作的原子性,這個時候就可以用鎖。
LongAdder
1、LongAdder采用了分段鎖的思想,內(nèi)部維護(hù)了一組計(jì)數(shù)單元,不同的線程可以操作不同的計(jì)數(shù)單元,減少了線程競爭,提高了效率。最終LongAdder的數(shù)值等于base值+所有計(jì)數(shù)單元的值之和。
2、在線程數(shù)量比較少時AtomicLong效率要高一點(diǎn),在線程數(shù)量很多時LongAdder效率高一點(diǎn)。
其它
假如有一個第三方接口,有很多個線程去調(diào)用獲取數(shù)據(jù),現(xiàn)在規(guī)定每秒鐘最多有10個線程同
時調(diào)用它,如何做到?
可以使用ScheduledThreadPool的scheduleAtFixedRate方法。
用三個線程按順序循環(huán)打印abc三個字母,比如abcabcabc?
如果讓你實(shí)現(xiàn)一個并發(fā)安全的鏈表,你會怎么做?
有哪些無鎖數(shù)據(jù)結(jié)構(gòu),他們實(shí)現(xiàn)的原理是什么?
CAS實(shí)現(xiàn)的
簡述ConcurrentLinkedQueue和LinkedBlockingQueue的用處和不同之處?
BlockingQue哪些操作時阻塞的
take和put是阻塞的。
poll和offer是非阻塞的。