我們現(xiàn)在所使用操作系統(tǒng)都是多任務(wù)操作系統(tǒng)(早期使用的DOS操作系統(tǒng)為單任務(wù)操作系統(tǒng)),多任務(wù)操作指在同一時(shí)刻可以同時(shí)做多件事(可以同時(shí)執(zhí)行多個(gè)程序)。
多進(jìn)程:每個(gè)程序都是一個(gè)進(jìn)程,在操作系統(tǒng)中可以同時(shí)執(zhí)行多個(gè)程序,多進(jìn)程的目的是為了有效的使用CPU資源,每開一個(gè)進(jìn)程系統(tǒng)要為該進(jìn)程分配相關(guān)的系統(tǒng)資源(內(nèi)存資源);
多線程:線程是進(jìn)程內(nèi)部比進(jìn)程更小的執(zhí)行單元(執(zhí)行流|程序片段),每個(gè)線程完成一個(gè)任務(wù),每個(gè)進(jìn)程內(nèi)部包含了多個(gè)線程每個(gè)線程做自己的事情,在進(jìn)程中的所有線程共享該進(jìn)程的資源;
主線程:在進(jìn)程中至少存在一個(gè)主線程,其他子線程都由主線程開啟,主線程不一定在其他線程結(jié)束后結(jié)束,有可能在其他線程結(jié)束前結(jié)束。Java中的主線程是main線程,是Java的main函數(shù);
當(dāng)應(yīng)用場(chǎng)景為計(jì)算密集型時(shí):為了將每個(gè)cpu充分利用起來(lái),線程數(shù)量正常是cpu核數(shù)+1,還可以看jdk的使用版本,1.8版本中可以使用cpu核數(shù)*2。
當(dāng)應(yīng)用場(chǎng)景為io密集型時(shí):做web端開發(fā)的時(shí)候,涉及到大量的網(wǎng)絡(luò)傳輸,不進(jìn)入持,緩存和與數(shù)據(jù)庫(kù)交互也會(huì)存在大量io,當(dāng)發(fā)生io時(shí)候,線程就會(huì)停止,等待io結(jié)束,數(shù)據(jù)準(zhǔn)備好,線程才會(huì)繼續(xù)執(zhí)行,所以當(dāng)io密集時(shí),可以多創(chuàng)建點(diǎn)線程,讓線程等待時(shí)候,其他線程執(zhí)行,更高效的利用cpu效率,他有一個(gè)計(jì)算公式,套用公式的話,雙核cpu理想的線程數(shù)就是20。
采用多線程技術(shù)的應(yīng)用程序可以更好地利用系統(tǒng)資源。主要優(yōu)勢(shì)在于充分利用了CPU的空閑時(shí)間片,用盡可能少的時(shí)間來(lái)對(duì)用戶的要求做出響應(yīng),使得進(jìn)程的整體運(yùn)行效率得到較大提高,同時(shí)增強(qiáng)了應(yīng)用程序的靈活性。由于同一進(jìn)程的所有線程是共享同一內(nèi)存,所以不需要特殊的數(shù)據(jù)傳送機(jī)制,不需要建立共享存儲(chǔ)區(qū)或共享文件,從而使得不同任務(wù)之間的協(xié)調(diào)操作與運(yùn)行、數(shù)據(jù)的交互、資源的分配等問題更加易于解決。
多線程的目的就是為了能提高程序的執(zhí)行效率提高程序運(yùn)行速度,但是并發(fā)編程并不總是能提高程序運(yùn)行速度的,而且并發(fā)編程可能會(huì)遇到很多問題,比如:內(nèi)存泄漏、死鎖、線程不安全等等。
進(jìn)程:是正在運(yùn)行中的程序,是系統(tǒng)進(jìn)行資源調(diào)度和分配的的基本單位。
線程:是進(jìn)程的子任務(wù),是任務(wù)調(diào)度和執(zhí)行的基本單位;
一個(gè)程序至少有一個(gè)進(jìn)程,一個(gè)進(jìn)程至少有一個(gè)線程,線程依賴于進(jìn)程而存在;
進(jìn)程在執(zhí)行過程中擁有獨(dú)立的內(nèi)存單元,而多個(gè)線程共享進(jìn)程的內(nèi)存。線程與進(jìn)程相似,但線程是一個(gè)比進(jìn)程更小的執(zhí)行單位。
一個(gè)進(jìn)程在其執(zhí)行的過程中可以產(chǎn)生多個(gè)線程。與進(jìn)程不同的是同類的多個(gè)線程共享進(jìn)程的堆和方法區(qū)資源,但每個(gè)線程有自己的程序計(jì)數(shù)器、虛擬機(jī)棧和本地方法棧,所以系統(tǒng)在產(chǎn)生一個(gè)線程,或是在各個(gè)線程之間作切換工作時(shí),負(fù)擔(dān)要比進(jìn)程小得多,也正因?yàn)槿绱耍€程也被稱為輕量級(jí)進(jìn)程。
a.繼承 Thread 類;b.實(shí)現(xiàn) Runnable 接口;c. 實(shí)現(xiàn)Callable接口;d. 使用線程池。
我們可以通過繼承Thread類或者調(diào)用Runnable接口來(lái)實(shí)現(xiàn)線程,因?yàn)镴ava不支持類的多重繼承,但允許你調(diào)用多個(gè)接口。所以如果你想要繼承其他的類,當(dāng)然是調(diào)用Runnable接口好了。
如果想讓線程池執(zhí)行任務(wù)的話需要實(shí)現(xiàn)的Runnable接口或Callable接口。 Runnable接口或Callable接口實(shí)現(xiàn)類都可以被ThreadPoolExecutor或ScheduledThreadPoolExecutor執(zhí)行。兩者的區(qū)別在于 Runnable 接口不會(huì)返回結(jié)果但是 Callable 接口可以返回結(jié)果。
啟動(dòng)一個(gè)線程需要調(diào)用 Thread 對(duì)象的 start() 方法;
調(diào)用線程的 start() 方法后,線程處于可運(yùn)行狀態(tài),此時(shí)它可以由 JVM 調(diào)度并執(zhí)行,這并不意味著線程就會(huì)立即運(yùn)行;
run() 方法是線程運(yùn)行時(shí)由 JVM 回調(diào)的方法,無(wú)需手動(dòng)寫代碼調(diào)用;
直接調(diào)用線程的 run() 方法,相當(dāng)于在調(diào)用線程里繼續(xù)調(diào)用了一個(gè)普通的方法,并未啟動(dòng)一個(gè)新的線程。
線程通常有五種狀態(tài):創(chuàng)建,就緒,運(yùn)行,阻塞和死亡狀態(tài)
(1)創(chuàng)建狀態(tài)(New):新創(chuàng)建了一個(gè)線程對(duì)象。
(2)就緒狀態(tài)(Runnable):線程對(duì)象創(chuàng)建后,其他線程調(diào)用了該對(duì)象的start()方法。該狀態(tài)的線程位于可運(yùn)行線程池中,變得可運(yùn)行,等待獲取CPU的使用權(quán)。
(3)運(yùn)行狀態(tài)(Running):就緒狀態(tài)的線程獲取了CPU,執(zhí)行程序代碼。
(4)阻塞狀態(tài)(Blocked):阻塞狀態(tài)是線程因?yàn)槟撤N原因放棄CPU使用權(quán),暫時(shí)停止運(yùn)行。直到線程進(jìn)入就緒狀態(tài),才有機(jī)會(huì)轉(zhuǎn)到運(yùn)行狀態(tài)。阻塞的情況分三種:
(一)等待阻塞:運(yùn)行的線程執(zhí)行wait()方法,JVM會(huì)把該線程放入等待池中。(wait會(huì)釋放持有的鎖)
(二)同步阻塞:運(yùn)行的線程在獲取對(duì)象的同步鎖時(shí),若該同步鎖被別的線程占用,則JVM會(huì)把該線程放入鎖池中。
(三)其他阻塞:運(yùn)行的線程執(zhí)行sleep()或join()方法,或者發(fā)出了I/O請(qǐng)求時(shí),JVM會(huì)把該線程置為阻塞狀態(tài)。當(dāng)sleep()狀態(tài)超時(shí)、join()等待線程終止或者超時(shí)、或者I/O處理完畢時(shí),線程重新轉(zhuǎn)入就緒狀態(tài)。(注意,sleep是不會(huì)釋放持有的鎖)
(5)死亡狀態(tài)(Dead):線程執(zhí)行完了或者因異常退出了run()方法,該線程結(jié)束生命周期。
多線程編程中一般線程的個(gè)數(shù)都大于 CPU 核心的個(gè)數(shù),而一個(gè) CPU 核心在任意時(shí)刻只能被一個(gè)線程使用,為了讓這些線程都能得到有效執(zhí)行,CPU 采取的策略是為每個(gè)線程分配時(shí)間片并輪轉(zhuǎn)的形式。當(dāng)一個(gè)線程的時(shí)間片用完的時(shí)候就會(huì)重新處于就緒狀態(tài)讓給其他線程使用,這個(gè)過程就屬于一次上下文切換。
概括來(lái)說就是:當(dāng)前任務(wù)在執(zhí)行完 CPU 時(shí)間片切換到另一個(gè)任務(wù)之前會(huì)先保存自己的狀態(tài),以便下次再切換回這個(gè)任務(wù)時(shí),可以再加載這個(gè)任務(wù)的狀態(tài)。任務(wù)從保存到再加載的過程就是一次上下文切換。
上下文切換通常是計(jì)算密集型的。也就是說,它需要相當(dāng)可觀的處理器時(shí)間,在每秒幾十上百次的切換中,每次切換都需要納秒量級(jí)的時(shí)間。所以,上下文切換對(duì)系統(tǒng)來(lái)說意味著消耗大量的 CPU 時(shí)間,事實(shí)上,可能是操作系統(tǒng)中時(shí)間消耗最大的操作。
Linux 相比與其他操作系統(tǒng)(包括其他類 Unix 系統(tǒng))有很多的優(yōu)點(diǎn),其中有一項(xiàng)就是,其上下文切換和模式切換的時(shí)間消耗非常少。
使用Thread類的setDaemon(true)方法可以將線程設(shè)置為守護(hù)線程,需要注意的是,需要在調(diào)用start()方法前調(diào)用這個(gè)方法,否則會(huì)拋出IllegalThreadStateException異常。
當(dāng)我們?cè)贘ava程序中創(chuàng)建一個(gè)線程,它就被稱為用戶線程。一個(gè)守護(hù)線程是在后臺(tái)執(zhí)行并且不會(huì)阻止JVM終止的線程。當(dāng)沒有用戶線程在運(yùn)行的時(shí)候,JVM關(guān)閉程序并且退出。一個(gè)守護(hù)線程創(chuàng)建的子線程依然是守護(hù)線程。
Thread類的sleep()和yield()方法將在當(dāng)前正在執(zhí)行的線程上運(yùn)行。所以在其他處于等待狀態(tài)的線程上調(diào)用這些方法是沒有意義的。這就是為什么這些方法是靜態(tài)的。它們可以在當(dāng)前正在執(zhí)行的線程中工作,并避免程序員錯(cuò)誤的認(rèn)為可以在其他非運(yùn)行線程調(diào)用這些方法。
Windows系統(tǒng)下執(zhí)行java -jar arthas-boot.jar
Linux系統(tǒng)下解壓arthas,執(zhí)行ps -ef | grep java找出java進(jìn)程pid數(shù)字
new 一個(gè) Thread,線程進(jìn)入了新建狀態(tài);調(diào)用 start() 方法,會(huì)啟動(dòng)一個(gè)線程并使線程進(jìn)入了就緒狀態(tài),當(dāng)分配到時(shí)間片后就可以開始運(yùn)行了。 start() 會(huì)執(zhí)行線程的相應(yīng)準(zhǔn)備工作,然后自動(dòng)執(zhí)行 run() 方法的內(nèi)容,這是真正的多線程工作。 而直接執(zhí)行 run() 方法,會(huì)把 run 方法當(dāng)成一個(gè) main 線程下的普通方法去執(zhí)行,并不會(huì)在某個(gè)線程中執(zhí)行它,所以這并不是多線程工作。
總結(jié): 調(diào)用 start 方法方可啟動(dòng)線程并使線程進(jìn)入就緒狀態(tài),而 run 方法只是 thread 的一個(gè)普通方法調(diào)用,還是在主線程里執(zhí)行.
Callable接口類似于Runnable,從名字就可以看出來(lái),但是Runnable不會(huì)返回結(jié)果,并且無(wú)法拋出返回結(jié)果的異常,而Callable功能更強(qiáng)大一些,被線程執(zhí)行后,可以返回值,這個(gè)返回值可以被Future拿到,也就是說,F(xiàn)uture可以拿到異步執(zhí)行任務(wù)的返回值。可以認(rèn)為是帶有返回值的Runnable.Future接口表示異步任務(wù),是還沒有完成的任務(wù)給出的未來(lái)結(jié)果。所以說Callable用于產(chǎn)生結(jié)果,F(xiàn)uture用于獲取結(jié)果。
(1) 搶占式調(diào)度策略
Java運(yùn)行時(shí)系統(tǒng)的線程調(diào)度算法是搶占式的 (preemptive)。Java運(yùn)行時(shí)系統(tǒng)支持一種簡(jiǎn)單的固定優(yōu)先級(jí)的調(diào)度算法。如果一個(gè)優(yōu)先級(jí)比其他任何處于可運(yùn)行狀態(tài)的線程都高的線程進(jìn)入就緒狀態(tài),那么運(yùn)行時(shí)系統(tǒng)就會(huì)選擇該線程運(yùn)行。新的優(yōu)先級(jí)較高的線程搶占(preempt)了其他線程。但是Java運(yùn)行時(shí)系統(tǒng)并不搶占同優(yōu)先級(jí)的線程。換句話說,Java運(yùn)行時(shí)系統(tǒng)不是分時(shí)的(time-slice)。然而,基于Java Thread類的實(shí)現(xiàn)系統(tǒng)可能是支持分時(shí)的,因此編寫代碼時(shí)不要依賴分時(shí)。當(dāng)系統(tǒng)中的處于就緒狀態(tài)的線程都具有相同優(yōu)先級(jí)時(shí),線程調(diào)度程序采用一種簡(jiǎn)單的、非搶占式的輪轉(zhuǎn)的調(diào)度順序。
(2) 時(shí)間片輪轉(zhuǎn)調(diào)度策略
有些系統(tǒng)的線程調(diào)度采用時(shí)間片輪轉(zhuǎn)(round-robin)調(diào)度策略。這種調(diào)度策略是從所有處于就緒狀態(tài)的線程中選擇優(yōu)先級(jí)最高的線程分配一定的CPU時(shí)間運(yùn)行。該時(shí)間過后再選擇其他線程運(yùn)行。只有當(dāng)線程運(yùn)行結(jié)束、放棄(yield)CPU或由于某種原因進(jìn)入阻塞狀態(tài),低優(yōu)先級(jí)的線程才有機(jī)會(huì)執(zhí)行。如果有兩個(gè)優(yōu)先級(jí)相同的線程都在等待CPU,則調(diào)度程序以輪轉(zhuǎn)的方式選擇運(yùn)行的線程。
搶占式。一個(gè)線程用完CPU之后,操作系統(tǒng)會(huì)根據(jù)線程優(yōu)先級(jí)、線程饑餓情況等數(shù)據(jù)算出一個(gè)總的優(yōu)先級(jí)并分配下一個(gè)時(shí)間片給某個(gè)線程執(zhí)行。
線程調(diào)度器是一個(gè)操作系統(tǒng)服務(wù),它負(fù)責(zé)為 Runnable 狀態(tài)的線程分配 CPU 時(shí)間。一旦我們創(chuàng)建一個(gè)線程并啟動(dòng)它,它的執(zhí)行便依賴于線程調(diào)度器的實(shí)現(xiàn)。同上一個(gè)問題,線程調(diào)度并不受到 Java 虛擬機(jī)控制,所以由應(yīng)用程序來(lái)控制它是 更好的選擇(也就是說不要讓你的程序依賴于線程的優(yōu)先級(jí))。時(shí)間分片是指將可用的 CPU 時(shí)間分配給可用的 Runnable 線程的過程。分配 CPU 時(shí)間可以基于線程優(yōu)先級(jí)或者線程等待的時(shí)間。
兩者最主要的區(qū)別在于:sleep 方法沒有釋放鎖,而 wait 方法釋放了鎖 。
兩者都可以暫停線程的執(zhí)行。
wait 通常被用于線程間交互/通信,sleep 通常被用于暫停執(zhí)行。
wait() 方法被調(diào)用后,線程不會(huì)自動(dòng)蘇醒,需要?jiǎng)e的線程調(diào)用同一個(gè)對(duì)象上的 notify() 或者 notifyAll() 方法。sleep() 方法執(zhí)行完成后,線程會(huì)自動(dòng)蘇醒。或者可以使用 wait(long timeout)超時(shí)后線程會(huì)自動(dòng)蘇醒。
因?yàn)檫@些方法的調(diào)用是依賴鎖對(duì)象,而同步代碼塊的鎖對(duì)象是任意。鎖而Object代表任意的對(duì)象,所以定義在這里面。
使當(dāng)前線程從執(zhí)行狀態(tài)(運(yùn)行狀態(tài))變?yōu)榭蓤?zhí)行態(tài)(就緒狀態(tài))。當(dāng)前線程到了就緒狀態(tài),那么接下來(lái)具體是哪個(gè)個(gè)線程會(huì)從就緒狀態(tài)變成執(zhí)行狀態(tài)就要看系統(tǒng)的分配了。
Thread類的sleep()和yield()方法將在當(dāng)前正在執(zhí)行的線程上運(yùn)行。所以在其他處于等待狀態(tài)的線程上調(diào)用這些方法是沒有意義的。這就是為什么這些方法是靜態(tài)的。它們可以在當(dāng)前正在執(zhí)行的線程中工作,并避免程序員錯(cuò)誤的認(rèn)為可以在其他非運(yùn)行線程調(diào)用這些方法。
Thread類的sleep()和yield()方法將在當(dāng)前正在執(zhí)行的線程上運(yùn)行。所以在其他處于等待狀態(tài)的線程上調(diào)用這些方法是沒有意義的。這就是為什么這些方法是靜態(tài)的。它們可以在當(dāng)前正在執(zhí)行的線程中工作,并避免程序員錯(cuò)誤的認(rèn)為可以在其他非運(yùn)行線程調(diào)用這些方法。
線程在運(yùn)行過程中,有些時(shí)候可能需要中斷一些阻塞的線程,類Thread中提供了幾種中斷線程的方法,其中Thread.suspend()和Thread.stop()方法已經(jīng)過時(shí)了,因?yàn)檫@兩個(gè)方法是不安全的。Thread.stop(),會(huì)直接終止該線程,并且會(huì)立即釋放這個(gè)線程持有的所有鎖,而這些鎖恰恰是用來(lái)維持?jǐn)?shù)據(jù)的一致性的,如果此時(shí)。寫線程寫入數(shù)據(jù)時(shí)寫到一半,并強(qiáng)行終止,由于此時(shí)對(duì)象鎖已經(jīng)被釋放,另一個(gè)等待該鎖的讀線程就會(huì)讀到這個(gè)不一致的對(duì)象。Thread.suspend()會(huì)導(dǎo)致死鎖,Thread.resume()也不能使用。
Java 提供了很豐富的API 但沒有為停止線程提供 API。JDK 1.0 本來(lái)有一些像stop(), suspend() 和 resume()的控制方法但是由于潛在的死鎖威脅因此在后續(xù)的JDK 版本中他們被棄用了.之后Java API 的設(shè)計(jì)者就沒有提供一個(gè)兼容且線程安全的方法來(lái)停止一個(gè)線程。當(dāng)run() 或者 call() 方法執(zhí)行完的時(shí)候線程會(huì)自動(dòng)結(jié)束, 如果要手動(dòng)結(jié)束一個(gè)線程.你可以用volatile 布爾變量來(lái)退出 run()方法的循環(huán)或者是取消任務(wù)來(lái)中斷線程。
interrupted:查詢當(dāng)前線程的中斷狀態(tài),并且清除原狀態(tài)。如果一個(gè)線程被中斷了,第一次調(diào)用 interrupted 則返回 true,第二次和后面的就返回 false 了。
isInterrupted僅僅是查詢當(dāng)前線程的中斷狀態(tài)。
1)notify只會(huì)隨機(jī)選取一個(gè)處于等待池中的線程進(jìn)入鎖池去競(jìng)爭(zhēng)獲取鎖的機(jī)會(huì);
2)notifyAll會(huì)讓所有處于等待池的線程全部進(jìn)入鎖池去競(jìng)爭(zhēng)獲取鎖的機(jī)會(huì);
每一個(gè)線程都是有優(yōu)先級(jí)的.一般來(lái)說.高優(yōu)先級(jí)的線程在運(yùn)行時(shí)會(huì)具有優(yōu)先權(quán). 但這依賴于線程調(diào)度的實(shí)現(xiàn).這個(gè)實(shí)現(xiàn)是和操作系統(tǒng)相關(guān)的(OS dependent)。我們可以定義線程的優(yōu)先級(jí).但是這并不能保證高優(yōu)先級(jí)的線程會(huì)在低優(yōu)先級(jí)的線程前執(zhí)行。線程優(yōu)先級(jí)是一個(gè)int 變量(從 1-10).1 代表最低優(yōu)先級(jí).10 代表最高優(yōu)先級(jí)。
線程類的構(gòu)造方法、靜態(tài)塊是被new這個(gè)線程類所在的線程所調(diào)用的,而run方法里面的代碼才是被線程自身所調(diào)用的。
舉個(gè)例子,假設(shè)Thread2中new了Thread1,main函數(shù)中new了Thread2,那么:
(1)Thread2的構(gòu)造方法、靜態(tài)塊是main線程調(diào)用的,Thread2的run()方法是Thread2自己調(diào)用的;
(2)Thread1的構(gòu)造方法、靜態(tài)塊是Thread2調(diào)用的,Thread1的run()方法是Thread1自己調(diào)用的;
簡(jiǎn)單的說,如果異常沒有被捕獲該線程將會(huì)停止執(zhí)行。Thread.UncaughtExceptionHandler 是用于處理未捕獲異常造成線程突然中斷情況的一個(gè)內(nèi)嵌接口。當(dāng)一個(gè)未捕獲異常將造成線程中斷的時(shí)候JVM 會(huì)使用Thread.getUncaughtExceptionHandler()來(lái)查詢線程的UncaughtExceptionHandler 并將線程和異常作為參數(shù)傳遞給handler 的uncaughtException()方法進(jìn)行處理。
(1)線程的生命周期開銷非常高
(2)消耗過多的CPU 資源
如果可運(yùn)行的線程數(shù)量多于可用處理器的數(shù)量,那么有線程將會(huì)被閑置。大量空閑的線程會(huì)占用許多內(nèi)存,給垃圾回收器帶來(lái)壓力,而且大量的線程在競(jìng)爭(zhēng)CPU 資源時(shí)還將產(chǎn)生其他性能的開銷。
(3)降低穩(wěn)定性
JVM 在可創(chuàng)建線程的數(shù)量上存在一個(gè)限制,這個(gè)限制值將隨著平臺(tái)的不同而不同,并且承受著多個(gè)因素制約,包括JVM 的啟動(dòng)參數(shù)、Thread 構(gòu)造函數(shù)中請(qǐng)求棧的大小,以及底層操作系統(tǒng)對(duì)線程的限制等。如果破壞了這些限制,那么可能拋出OutOfMemoryError 異常。
wait();(強(qiáng)迫一個(gè)線程等待)
notify();(通知一個(gè)線程繼續(xù)執(zhí)行),
notifyAll()(所有線程繼續(xù)執(zhí)行),
sleep()(強(qiáng)迫一個(gè)線程睡眠N毫秒),
join()(等待線程終止)
yield()(線程讓步)等等;
FutureTask 表示一個(gè)異步運(yùn)算的任務(wù)。 FutureTask 里面可以傳入一個(gè) Callable 的具體實(shí)現(xiàn)類,可以對(duì)這
個(gè)異步運(yùn)算的任務(wù)的結(jié)果進(jìn)行等待獲取、判斷是否已經(jīng)完成、取消任務(wù)等操作。當(dāng)然,由于 FutureTask
也是 Runnable 接口的實(shí)現(xiàn)類,所以 FutureTask 也可以放入線程池中。
多個(gè)線程在正常情況下的運(yùn)行是互不干擾的,但是CUP對(duì)線程的切換是隨機(jī)的,這樣線程運(yùn)行的過程就脫離了我們的控制,如果我們想讓多個(gè)線程之間有規(guī)律的運(yùn)行,就需要線程通訊,線程之間通信的可以讓多個(gè)線程按照我們預(yù)期的運(yùn)行過程去執(zhí)行。
1)wait()和notify()
wait(): 當(dāng)前線程釋放鎖并且進(jìn)入等待狀態(tài)。
notify(): 喚醒當(dāng)前線程,上面wait() 的時(shí)候線程進(jìn)入了等待狀態(tài),如果我們想讓線程執(zhí)行需要通過notify()喚醒該線程。
notifyAll(): 喚醒所有進(jìn)入等待狀態(tài)的線程。
2)join()方法
join()方法的作用是使A線程加入B線程中執(zhí)行,B線程進(jìn)入阻塞狀態(tài),只有當(dāng)A線程運(yùn)行結(jié)束后B線程才會(huì)繼續(xù)執(zhí)行。
3)volatile關(guān)鍵字
volatile 關(guān)鍵字是實(shí)現(xiàn)線程變量之間真正共享的,就是我們理想中的共享狀態(tài),多個(gè)線程同時(shí)監(jiān)控著共享變量,當(dāng)變量發(fā)生變化時(shí)其它線程立即改變,具體實(shí)現(xiàn)與JMM內(nèi)存模型有關(guān)。
多個(gè)線程可以共享進(jìn)程的堆和方法區(qū)資源,既多個(gè)線程共享類變量。多個(gè)線程共享一個(gè)進(jìn)程的變量時(shí),如果線程對(duì)這個(gè)變量只有讀操作,沒有更新操作則這個(gè)線程沒有線程安全問題。如果線程需要對(duì)這個(gè)變量進(jìn)行修改操作,則可能會(huì)因?yàn)閿?shù)據(jù)更新不及時(shí)導(dǎo)致變量信息不準(zhǔn)確而引發(fā)線程不安全。
當(dāng)多個(gè)線程對(duì)同一個(gè)資源進(jìn)行操作的時(shí)候就會(huì)有線程安全。解決線程安全的核心思想就是加鎖。加鎖有兩種方式:1.JVM提供的鎖,就是synchronized鎖,即同步代碼和同步代碼塊 2.jdk提供的各種鎖,如lock。
在JDK1.1版本中,所有的集合都是線程安全的。但是在1.2及以后的版本中就出現(xiàn)了一些線程不安全的集合,為什么版本升級(jí)反而會(huì)出現(xiàn)一些線程不安全的集合呢?因?yàn)榫€程不安全的集合普遍比線程安全的集合效率高的多。隨著業(yè)務(wù)的發(fā)展,特別是在WEB應(yīng)用中,為了提高用戶體驗(yàn),減少用戶的等待時(shí)間,頁(yè)面的響應(yīng)速度(也就是效率)是優(yōu)先考慮的。而且對(duì)線程不安全的集合加鎖以后也能達(dá)到安全的效果(但是效率會(huì)低,因?yàn)闀?huì)有鎖的獲取以及等待)。其實(shí)在JDK源碼中相同效果的集合線程安全的比線程不安全的就多了一個(gè)同步機(jī)制,但是效率上卻低了不止一點(diǎn)點(diǎn),因?yàn)樾实停砸呀?jīng)不太建議使用了。下面列舉一些常用的線程安全的集合。
Vector:就比ArrayList多了個(gè)同步化機(jī)制。
HashTable:就比HashMap多了個(gè)線程安全。
ConcurrentHashMap:是一種高效但是線程安全的集合。
Stack:棧,線程安全,繼承與Vector。
1)修飾實(shí)例方法: 作用于當(dāng)前對(duì)象實(shí)例加鎖,進(jìn)入同步代碼前要獲得 當(dāng)前對(duì)象實(shí)例的鎖
synchronized void method() {
? //業(yè)務(wù)代碼
}
2)修飾靜態(tài)方法: 也就是給當(dāng)前類加鎖,會(huì)作用于類的所有對(duì)象實(shí)例 ,進(jìn)入同步代碼前要獲得 當(dāng)前 class 的鎖。因?yàn)殪o態(tài)成員不屬于任何一個(gè)實(shí)例對(duì)象,是類成員( static 表明這是該類的一個(gè)靜態(tài)資源,不管 new 了多少個(gè)對(duì)象,只有一份)。所以,如果一個(gè)線程 A 調(diào)用一個(gè)實(shí)例對(duì)象的非靜態(tài) synchronized 方法,而線程 B 需要調(diào)用這個(gè)實(shí)例對(duì)象所屬類的靜態(tài) synchronized 方法,是允許的,不會(huì)發(fā)生互斥現(xiàn)象,因?yàn)樵L問靜態(tài) synchronized 方法占用的鎖是當(dāng)前類的鎖,而訪問非靜態(tài) synchronized 方法占用的鎖是當(dāng)前實(shí)例對(duì)象鎖。
synchronized void staic method() {
? //業(yè)務(wù)代碼
}
3)修飾代碼塊 :指定加鎖對(duì)象,對(duì)給定對(duì)象/類加鎖。synchronized(this|object) 表示進(jìn)入同步代碼庫(kù)前要獲得給定對(duì)象的鎖。synchronized(類.class) 表示進(jìn)入同步代碼前要獲得 當(dāng)前 class 的鎖
synchronized(this) {
? //業(yè)務(wù)代碼
}
總結(jié):synchronized 關(guān)鍵字加到 static 靜態(tài)方法和 synchronized(class) 代碼塊上都是是給 Class 類上鎖。
synchronized 關(guān)鍵字加到實(shí)例方法上是給對(duì)象實(shí)例上鎖。
盡量不要使用 synchronized(String a) 因?yàn)?JVM 中,字符串常量池具有緩存功能!
構(gòu)造方法不能使用 synchronized 關(guān)鍵字修飾。構(gòu)造方法本身就屬于線程安全的,不存在同步的構(gòu)造方法一說。
public class Singleton {
private static Singleton uniqueInstance;
// 私有化構(gòu)造方法
private Singleton() {
}
// 提供getInstance方法
public static Singleton getInstance() {
//先判斷對(duì)象是否已經(jīng)實(shí)例過,沒有實(shí)例化過才進(jìn)入加鎖代碼
if (uniqueInstance == null) {
//類對(duì)象加鎖
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
同步:功能調(diào)用時(shí),在沒有得到結(jié)果之前,該調(diào)用就不返回或繼續(xù)執(zhí)行后續(xù)操作。這時(shí)程序是阻塞的,只有接收到返回的值或消息后才往下執(zhí)行其他的命令。因此,簡(jiǎn)單來(lái)說,同步就是必須一件一件做事,等前一件事做完了才能做完下一件事。
異步:與同步相對(duì),當(dāng)一個(gè)異步過程調(diào)用發(fā)出后,調(diào)用者在沒有得到結(jié)果之前,就可以繼續(xù)執(zhí)行后續(xù)操作,當(dāng)這個(gè)調(diào)用完成后,一般通過狀態(tài)或者回調(diào)來(lái)通知調(diào)用者。
同步塊,這意味著同步塊之外的代碼是異步執(zhí)行的,這比同步整個(gè)方法更提升代 碼的效率。請(qǐng)知道一條原則:同步的范圍越小越好。
當(dāng)一個(gè)線程需要調(diào)用對(duì)象的wait()方法的時(shí)候,這個(gè)線程必須擁有該對(duì)象的鎖,接著它就會(huì)釋放這個(gè)對(duì)象鎖并進(jìn)入等待狀態(tài)直到其他線程調(diào)用這個(gè)對(duì)象上的notify()方法。同樣的,當(dāng)一個(gè)線程需要調(diào)用對(duì)象的notify()方法時(shí),它會(huì)釋放這個(gè)對(duì)象的鎖,以便其他在等待的線程就可以得到這個(gè)對(duì)象鎖。由于所有的這些方法都需要線程持有對(duì)象的鎖,這樣就只能通過同步來(lái)實(shí)現(xiàn),所以他們只能在同步方法或者同步塊中被調(diào)用。
同步就是協(xié)同步調(diào),按預(yù)定的先后次序進(jìn)行運(yùn)行。如:你說完,我再說。這里的同步千萬(wàn)不要理解成那個(gè)同時(shí)進(jìn)行,應(yīng)是指協(xié)同、協(xié)助、互相配合。線程同步是指多線程通過特定的設(shè)置(如互斥量,事件對(duì)象,臨界區(qū))來(lái)控制線程之間的執(zhí)行順序(即所謂的同步)也可以說是在線程之間通過同步建立起執(zhí)行順序的關(guān)系,如果沒有同步,那線程之間是各自運(yùn)行各自的!
線程互斥是指對(duì)于共享的進(jìn)程系統(tǒng)資源,在各單個(gè)線程訪問時(shí)的排它性。當(dāng)有若干個(gè)線程都要使用某一共享資源時(shí),任何時(shí)刻最多只允許一個(gè)線程去使用,其它要使用該資源的線程必須等待,直到占用資源者釋放該資源。線程互斥可以看成是一種特殊的線程同步(下文統(tǒng)稱為同步)。
線程池就是提前創(chuàng)建若干個(gè)線程,如果有任務(wù)需要處理,線程池里的線程就會(huì)處理任務(wù),處理完之后線程并不會(huì)被銷毀,而是等待下一個(gè)任務(wù)。由于創(chuàng)建和銷毀線程都是消耗系統(tǒng)資源的,所以當(dāng)你想要頻繁的創(chuàng)建和銷毀線程的時(shí)候就可以考慮使用線程池來(lái)提升系統(tǒng)的性能。
java中經(jīng)常需要用到多線程來(lái)處理一些業(yè)務(wù),我們非常不建議單純使用繼承Thread或者實(shí)現(xiàn)Runnable接口的方式來(lái)創(chuàng)建線程,那樣勢(shì)必有創(chuàng)建及銷毀線程耗費(fèi)資源、線程上下文切換問題。同時(shí)創(chuàng)建過多的線程也可能引發(fā)資源耗盡的風(fēng)險(xiǎn),這個(gè)時(shí)候引入線程池比較合理,方便線程任務(wù)的管理。java中涉及到線程池的相關(guān)類均在jdk1.5開始的java.util.concurrent包中,涉及到的幾個(gè)核心類及接口包括:Executor、Executors、ExecutorService、ThreadPoolExecutor、FutureTask、Callable、Runnable等。
線程池提供了一種限制和管理資源(包括執(zhí)行一個(gè)任務(wù))。 每個(gè)線程池還維護(hù)一些基本統(tǒng)計(jì)信息,例如已完成任務(wù)的數(shù)量。
線程池的好處如下:
1.降低資源消耗。通過重復(fù)利用已創(chuàng)建的線程降低線程創(chuàng)建和銷毀造成的消耗。
2.可有效的控制最大并發(fā)線程數(shù),提高系統(tǒng)資源的使用率,同時(shí)避免過多資源競(jìng)爭(zhēng),避免堵塞。
3.提供定時(shí)執(zhí)行、定期執(zhí)行、單線程、并發(fā)數(shù)控制等功能。
1)線程提交到線程池
2)判斷核心線程池是否已經(jīng)達(dá)到設(shè)定的數(shù)量,如果沒有達(dá)到,則直接創(chuàng)建線程執(zhí)行任務(wù)
3)如果達(dá)到了,則放在隊(duì)列中,等待執(zhí)行
4)如果隊(duì)列已經(jīng)滿了,則判斷線程的數(shù)量是否已經(jīng)達(dá)到設(shè)定的最大值,如果達(dá)到了,則直接執(zhí)行拒絕策略
5)如果沒有達(dá)到,則創(chuàng)建線程執(zhí)行任務(wù)。
RUNNING :能接受新提交的任務(wù),并且也能處理阻塞隊(duì)列中的任務(wù);
SHUTDOWN:關(guān)閉狀態(tài),不再接受新提交的任務(wù),但卻可以繼續(xù)處理阻塞隊(duì)列中已保存的任務(wù)。在線程池處于 RUNNING 狀態(tài)時(shí),調(diào)用 shutdown()方法會(huì)使線程池進(jìn)入到該狀態(tài)。(finalize() 方法在執(zhí)行過程中也會(huì)調(diào)用shutdown()方法進(jìn)入該狀態(tài));
STOP:不能接受新任務(wù),也不處理隊(duì)列中的任務(wù),會(huì)中斷正在處理任務(wù)的線程。在線程池處于 RUNNING 或 SHUTDOWN 狀態(tài)時(shí),調(diào)用 shutdownNow() 方法會(huì)使線程池進(jìn)入到該狀態(tài);
TIDYING:如果所有的任務(wù)都已終止了,workerCount (有效線程數(shù)) 為0,線程池進(jìn)入該狀態(tài)后會(huì)調(diào)用 terminated() 方法進(jìn)入TERMINATED 狀態(tài)。
TERMINATED:在terminated() 方法執(zhí)行完后進(jìn)入該狀態(tài),默認(rèn)terminated()方法中什么也沒有做。
Executor就是一個(gè)線程池框架,Executor 位于java.util.concurrent.Executors ,提供了用于創(chuàng)建工作線程的線程池的工廠方法。它包含一組用于有效管理工作線程的組件。Executor API 通過 Executors 將任務(wù)的執(zhí)行與要執(zhí)行的實(shí)際任務(wù)解耦。
Executor 接口對(duì)象能執(zhí)行我們的線程任務(wù);
Executors 工具類的不同方法按照我們的需求創(chuàng)建了不同的線程池,來(lái)滿足業(yè)務(wù)的需求。
ExecutorService 接口繼承了Executor接口并進(jìn)行了擴(kuò)展,提供了更多的方法,我們能夠獲得任務(wù)執(zhí)行的狀態(tài)并且可以獲取任務(wù)的返回值。
1.newSingleThreadExecutor
創(chuàng)建一個(gè)單線程化的線程池,它只會(huì)用唯一的工作線程來(lái)執(zhí)行任務(wù),保證所有任務(wù)按照指定順序(FIFO, LIFO, 優(yōu)先級(jí))執(zhí)行。
2.newFixedThreadPoo
創(chuàng)建一個(gè)定長(zhǎng)線程池,可控制線程最大并發(fā)數(shù),超出的線程會(huì)在隊(duì)列中等待。
3.newCachedThreadPool
創(chuàng)建一個(gè)可緩存線程池,如果線程池長(zhǎng)度超過處理需要,可靈活回收空閑線程,若無(wú)可回收,則新建線程。
4.newScheduledThreadPool
創(chuàng)建一個(gè)定長(zhǎng)線程池,支持定時(shí)及周期性任務(wù)執(zhí)行。
1)newFixedThreadPool和newSingleThreadExecutor: 主要問題是堆積的請(qǐng)求處理隊(duì)列可能會(huì)耗費(fèi)非常大的內(nèi)存,甚至OOM。
2)newCachedThreadPool和newScheduledThreadPool: 主要問題是線程數(shù)最大數(shù)是Integer.MAX_VALUE,可能會(huì)創(chuàng)建數(shù)量非常多的線程,甚至OOM。
3)線程池不允許使用Executors去創(chuàng)建,而是通過ThreadPoolExecutor的方式,這樣的處理方式讓程序員更加明確線程池的運(yùn)行規(guī)則,規(guī)避資源耗盡的風(fēng)險(xiǎn)。
ThreadPoolExecutor是線程池的核心實(shí)現(xiàn)類,在JDK1.5引入,位于java.util.concurrent包。
通過下面的demo來(lái)了解ThreadPoolExecutor創(chuàng)建線程的過程。
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* 測(cè)試ThreadPoolExecutor對(duì)線程的執(zhí)行順序
**/
public class ThreadPoolSerialTest {
public static void main(String[] args) {
//核心線程數(shù)
int corePoolSize = 3;
//最大線程數(shù)
int maximumPoolSize = 6;
//超過 corePoolSize 線程數(shù)量的線程最大空閑時(shí)間
long keepAliveTime = 2;
//以秒為時(shí)間單位
TimeUnit unit = TimeUnit.SECONDS;
//創(chuàng)建工作隊(duì)列,用于存放提交的等待執(zhí)行任務(wù)
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<Runnable>(2);
ThreadPoolExecutor threadPoolExecutor = null;
try {
//創(chuàng)建線程池
threadPoolExecutor = new ThreadPoolExecutor(corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
workQueue,
new ThreadPoolExecutor.AbortPolicy());
//循環(huán)提交任務(wù)
for (int i = 0; i < 8; i++) {
//提交任務(wù)的索引
final int index = (i + 1);
threadPoolExecutor.submit(() -> {
//線程打印輸出
System.out.println("大家好,我是線程:" + index);
try {
//模擬線程執(zhí)行時(shí)間,10s
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
//每個(gè)任務(wù)提交后休眠500ms再提交下一個(gè)任務(wù),用于保證提交順序
Thread.sleep(500);
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
threadPoolExecutor.shutdown();
}
}
}
當(dāng)一個(gè)新任務(wù)被提交時(shí):
1. 當(dāng)前活躍線程數(shù)<corePoolSize,則創(chuàng)建一個(gè)新線程執(zhí)行新任務(wù);
2. 當(dāng)前活躍線程數(shù)>corePoolSize,且隊(duì)列(workQueue)未滿時(shí),則將新任務(wù)放入隊(duì)列中;
3. 當(dāng)前活躍線程數(shù)>corePoolSize,且隊(duì)列(workQueue)已滿,且當(dāng)前活躍線程數(shù)<maximumPoolSize,則繼續(xù)創(chuàng)建一個(gè)新線程執(zhí)行新任務(wù);
4. 當(dāng)前活躍線程數(shù)>corePoolSize,且隊(duì)列(workQueue)已滿,且當(dāng)前活躍線程數(shù)=maximumPoolSize,則執(zhí)行拒絕策略(handler);
當(dāng)任務(wù)執(zhí)行完成后:
1. 超出corePoolSize的空閑線程,在等待新任務(wù)時(shí),如果超出了keepAliveTime,則線程會(huì)被銷毀;
2. 如果allowCoreThreadTimeOut被設(shè)置為true,那么corePoolSize以內(nèi)的空閑線程,如果超出了keepAliveTime,則同樣會(huì)被銷毀。
corePoolSize就是線程池中的核心線程數(shù)量,這幾個(gè)核心線程在沒有用的時(shí)候,也不會(huì)被回收
maximumPoolSize就是線程池中可以容納的最大線程的數(shù)量
keepAliveTime就是線程池中除了核心線程之外的其他的最長(zhǎng)可以保留的時(shí)間,因?yàn)樵诰€程池中,除了核心線程即使在無(wú)任務(wù)的情況下也不能被清 除,其余的都是有存活時(shí)間的,意思就是非核心線程可以保留的最長(zhǎng)的空閑時(shí)間
util就是計(jì)算這個(gè)時(shí)間的一個(gè)單位。
workQueue就是等待隊(duì)列,任務(wù)可以儲(chǔ)存在任務(wù)隊(duì)列中等待被執(zhí)行,執(zhí)行的是FIFIO原則(先進(jìn)先出)。
threadFactory就是創(chuàng)建線程的線程工廠。
handler是一種拒絕策略,我們可以在任務(wù)滿了之后,拒絕執(zhí)行某些任務(wù)。
當(dāng)線程充滿了ThreadPool的有界隊(duì)列時(shí),飽和策略開始起作用。飽和策略可以理解為隊(duì)列飽和后,處理后續(xù)無(wú)法入隊(duì)的任務(wù)的策略。ThreadPoolExecutor可以通過調(diào)用setRejectedExecutionHandler來(lái)修改飽和策略。
當(dāng)請(qǐng)求任務(wù)不斷的過來(lái),而系統(tǒng)此時(shí)又處理不過來(lái)的時(shí)候,我們需要采取的策略是拒絕服務(wù)。RejectedExecutionHandler接口提供了拒絕任務(wù)處理的自定義方法的機(jī)會(huì)。在ThreadPoolExecutor中已經(jīng)包含四種處理策略。
AbortPolicy策略:該策略會(huì)直接拋出異常,阻止系統(tǒng)正常工作。
CallerRunsPolicy 策略:只要線程池未關(guān)閉,該策略直接在調(diào)用者線程中,運(yùn)行當(dāng)前的被丟棄的任務(wù)。
DiscardOleddestPolicy策略:該策略將丟棄最老的一個(gè)請(qǐng)求,也就是即將被執(zhí)行的任務(wù),并嘗試再次提交當(dāng)前任務(wù)。
DiscardPolicy策略:該策略默默的丟棄無(wú)法處理的任務(wù),不予任何處理。
除了JDK默認(rèn)提供的四種拒絕策略,我們可以根據(jù)自己的業(yè)務(wù)需求去自定義拒絕策略,自定義的方式很簡(jiǎn)單,直接實(shí)現(xiàn)RejectedExecutionHandler接口即可。
1.execute()方法用于提交不需要返回值的任務(wù),所以無(wú)法判斷任務(wù)是否被線程池執(zhí)行成功與否;
2.submit()方法用于提交需要返回值的任務(wù)。線程池會(huì)返回一個(gè) Future 類型的對(duì)象,通過這個(gè) Future 對(duì)象可以判斷任務(wù)是否執(zhí)行成功,并且可以通過 Future 的 get()方法來(lái)獲取返回值,get()方法會(huì)阻塞當(dāng)前線程直到任務(wù)完成,而使用 get(long timeout,TimeUnit unit)方法則會(huì)阻塞當(dāng)前線程一段時(shí)間后立即返回,這時(shí)候有可能任務(wù)沒有執(zhí)行完。
線程組ThreadGroup對(duì)象中的stop,resume,suspend會(huì)導(dǎo)致安全問題,主要是死鎖問題,已經(jīng)被官方廢棄,多以價(jià)值已經(jīng)大不如以前。
線程組ThreadGroup不是線程安全的,在使用過程中不能及時(shí)獲取安全的信息。
shutdownNow():立即關(guān)閉線程池(暴力),正在執(zhí)行中的及隊(duì)列中的任務(wù)會(huì)被中斷,同時(shí)該方法會(huì)返回被中斷的隊(duì)列中的任務(wù)列表;
shutdown():平滑關(guān)閉線程池,正在執(zhí)行中的及隊(duì)列中的任務(wù)能執(zhí)行完成,后續(xù)進(jìn)來(lái)的任務(wù)會(huì)被執(zhí)行拒絕策略;
isTerminated():當(dāng)正在執(zhí)行的任務(wù)及對(duì)列中的任務(wù)全部都執(zhí)行(清空)完就會(huì)返回true;
線程池將線程和任務(wù)進(jìn)行解耦,線程是線程,任務(wù)是任務(wù),擺脫了之前通過 Thread 創(chuàng)建線程時(shí)的一個(gè)線程必須對(duì)應(yīng)一個(gè)任務(wù)的限制。在線程池中,同一個(gè)線程可以從阻塞隊(duì)列中不斷獲取新任務(wù)來(lái)執(zhí)行,其核心原理在于線程池對(duì) Thread 進(jìn)行了封裝,并不是每次執(zhí)行任務(wù)都會(huì)調(diào)用 Thread.start() 來(lái)創(chuàng)建新線程,而是讓每個(gè)線程去執(zhí)行一個(gè)“循環(huán)任務(wù)”,在這個(gè)“循環(huán)任務(wù)”中不停的檢查是否有任務(wù)需要被執(zhí)行,如果有則直接執(zhí)行,也就是調(diào)用任務(wù)中的 run 方法,將 run 方法當(dāng)成一個(gè)普通的方法執(zhí)行,通過這種方式將只使用固定的線程就將所有任務(wù)的 run 方法串聯(lián)起來(lái)。
1.ArrayBlockingQueue是一個(gè)基于數(shù)組結(jié)構(gòu)的有界阻塞隊(duì)列,此隊(duì)列按 FIFO(先進(jìn)先出)原則對(duì)元素進(jìn)行排序。
2.LinkedBlockingQueue一個(gè)基于鏈表結(jié)構(gòu)的阻塞隊(duì)列,此隊(duì)列按FIFO (先進(jìn)先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。靜態(tài)工廠方法Executors.newFixedThreadPool()使用了這個(gè)隊(duì)列
3.SynchronousQueue 一個(gè)不存儲(chǔ)元素的阻塞隊(duì)列。每個(gè)插入操作必須等到另一個(gè)線程調(diào)用移除操作,否則插入操作一直處于阻塞狀態(tài),吞吐量通常要高于LinkedBlockingQueue,靜態(tài)工廠方法Executors.newCachedThreadPool()使用了這個(gè)隊(duì)列。
4.PriorityBlockingQueue 一個(gè)具有優(yōu)先級(jí)的無(wú)限阻塞隊(duì)列。
首先是利用好SpringBoot的自動(dòng)裝配功能,配置好線程池的一些基本參數(shù)。
@Configuration
@EnableAsync
public class ThreadPoolTaskConfig {
/*
* 線程池名前綴
*/
private static final String threadNamePrefix = "Api-Async-";
/**
* bean的名稱, 默認(rèn)為首字母小寫的方法名
* @return
*/
@Bean("taskExecutor")
public ThreadPoolTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
/**
* 默認(rèn)情況下,在創(chuàng)建了線程池后,線程池中的線程數(shù)為0,當(dāng)有任務(wù)來(lái)之后,就會(huì)創(chuàng)建一個(gè)線程去執(zhí)行任務(wù),
* 當(dāng)線程池中的線程數(shù)目達(dá)到corePoolSize后,就會(huì)把到達(dá)的任務(wù)放到緩存隊(duì)列當(dāng)中;
* 當(dāng)隊(duì)列滿了,就繼續(xù)創(chuàng)建線程,當(dāng)線程數(shù)量大于等于maxPoolSize后,開始使用拒絕策略拒絕
*/
/*
* 核心線程數(shù)(默認(rèn)線程數(shù))
*/
executor.setCorePoolSize(corePoolSize);
//最大線程數(shù)
executor.setMaxPoolSize(maxPoolSize);
//緩沖隊(duì)列數(shù)
executor.setQueueCapacity(queueCapacity);
//允許線程空閑時(shí)間(單位是秒)
executor.setKeepAliveSeconds(keepAliveTime);
executor.setThreadNamePrefix(threadNamePrefix);
//用來(lái)設(shè)置線程池關(guān)閉時(shí)候等待所有任務(wù)都完成再繼續(xù)銷毀其他的Bean
executor.setWaitForTasksToCompleteOnShutdown(true);
//線程池對(duì)拒絕任務(wù)的處理策略,CallerRunsPolicy:由調(diào)用線程(提交任務(wù)的線程)處理該任務(wù)
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
//初始化
executor.initialize();
return executor;
}
}
配置好線程池的基本參數(shù)時(shí)候,我們就可以使用線程池了, 只要在一個(gè)限定域?yàn)閜ublic的方法頭部加上@Async注解即可。
@Async
public void createOrder() {
System.out.println("執(zhí)行任務(wù)");
}
1)分析任務(wù)的特性
任務(wù)的性質(zhì):CPU 密集型任務(wù)、IO 密集型任務(wù)和混合型任務(wù)。
任務(wù)的優(yōu)先級(jí):高、中、低。
任務(wù)的執(zhí)行時(shí)間:長(zhǎng)、中、短。
任務(wù)的依賴性:是否依賴其他系統(tǒng)資源,如數(shù)據(jù)庫(kù)連接。
2)具體策略
[1]CPU 密集型任務(wù)配置盡可能小的線程,如配置N(CPU核心數(shù))+1個(gè)線程的線程池。
[2]IO 密集型任務(wù)則由于線程并不是一直在執(zhí)行任務(wù),則配置盡可能多的線程,如2*N(CPU核心數(shù))。
[3]混合型任務(wù)如果可以拆分,則將其拆分成一個(gè) CPU 密集型任務(wù)和一個(gè) IO 密集型任務(wù)。只要這兩個(gè)任務(wù)執(zhí)行的時(shí)間相差不是太大,那么分解后執(zhí)行的吞吐率要高于串行執(zhí)行的吞吐率;如果這兩個(gè)任務(wù)執(zhí)行時(shí)間相差太大,則沒必要進(jìn)行分解。
[4]優(yōu)先級(jí)不同的任務(wù)可以使用優(yōu)先級(jí)隊(duì)列 PriorityBlockingQueue 來(lái)處理,它可以讓優(yōu)先級(jí)高的任務(wù)先得到執(zhí)行。但是,如果一直有高優(yōu)先級(jí)的任務(wù)加入到阻塞隊(duì)列中,那么低優(yōu)先級(jí)的任務(wù)可能永遠(yuǎn)不能執(zhí)行。
[5]執(zhí)行時(shí)間不同的任務(wù)可以交給不同規(guī)模的線程池來(lái)處理,或者也可以使用優(yōu)先級(jí)隊(duì)列,讓執(zhí)行時(shí)間短的任務(wù)先執(zhí)行。
[6]依賴數(shù)據(jù)庫(kù)連接池的任務(wù),因?yàn)榫€程提交 SQL 后需要等待數(shù)據(jù)庫(kù)返回結(jié)果,線程數(shù)應(yīng)該設(shè)置得較大,這樣才能更好的利用 CPU。
[7]建議使用有界隊(duì)列,有界隊(duì)列能增加系統(tǒng)的穩(wěn)定性和預(yù)警能力。可以根據(jù)需要設(shè)大一點(diǎn),比如幾千。使用無(wú)界隊(duì)列,線程池的隊(duì)列就會(huì)越來(lái)越大,有可能會(huì)撐滿內(nèi)存,導(dǎo)致整個(gè)系統(tǒng)不可用。
從字面上理解ThreadLocal就是“線程局部變量”的意思。簡(jiǎn)單的說就是,一個(gè)ThreadLocal在一個(gè)線程中是共享的,在不同線程之間又是隔離的(每個(gè)線程都只能看到自己線程的值)。
學(xué)習(xí)一個(gè)類之前我們需要了解一個(gè)類的API,這也是我們學(xué)習(xí)類的入口。而ThreadLocal類的API相當(dāng)簡(jiǎn)單。
在這里面比較重要的就是,get、set、remove了,這三個(gè)方法是對(duì)這個(gè)變量進(jìn)行操作的關(guān)鍵。set用于賦值操作,get用于獲取變量中的值,remove就是刪除當(dāng)前這個(gè)變量的值。需要注意的是initialValue方法會(huì)在第一次調(diào)用時(shí)被觸發(fā),用于初始化當(dāng)前變量值,例如在下列代碼中我們需要?jiǎng)?chuàng)建一個(gè)ThreadLocal,用于創(chuàng)建一個(gè)與線程綁定的Connection對(duì)象:
ThreadLocal connection = new ThreadLocal(){
? ? public Connection initialValue(){
? ? ? ? return DriverManager.getConnection(…);
? ? }
});
為什么我們將ThreadLocal說成變量,我們姑且可以這么理解,每個(gè)ThreadLocal實(shí)例中都可以保存一個(gè)值(基本數(shù)據(jù)類型值或者引用類型的引用值),而內(nèi)部保存的值是可以修改的,而這樣的特性與變量的特性及其相似,變量不就是用來(lái)保存一個(gè)值的嗎?也就是說每一個(gè)ThreadLocal實(shí)例就類似于一個(gè)變量名,不同的ThreadLocal實(shí)例就是不同的變量名,它們內(nèi)部會(huì)存有一個(gè)值(暫時(shí)這么理解)在后面的描述中所說的“ThreadLocal變量或者是線程變量”代表的就是ThreadLocal類的實(shí)例。我們通過重寫initialValue方法指定ThreadLocal變量的初始值,默認(rèn)情況下initialValue返回的是null。
接下來(lái)我們就來(lái)動(dòng)手實(shí)踐一下,來(lái)理解前面沒有理解的那句話:一個(gè)ThreadLocal在一個(gè)線程中是共享的,在不同線程之間又是隔離的(每個(gè)線程都只能看到自己線程的值)
public class ThreadLocalTest {
?? ?private static ThreadLocal<Integer> num = new ThreadLocal<Integer>() {
?? ??? ?// 重寫這個(gè)方法,可以修改“線程變量”的初始值,默認(rèn)是null
?? ??? ?@Override
?? ??? ?protected Integer initialValue() {
?? ??? ??? ?return 0;
?? ??? ?}
?? ?};
?
?? ?public static void main(String[] args) {
?? ??? ?// 創(chuàng)建一號(hào)線程
?? ??? ?new Thread(new Runnable() {
?? ??? ??? ?@Override
?? ??? ??? ?public void run() {
?? ??? ??? ??? ?// 在一號(hào)線程中將ThreadLocal變量設(shè)置為1
?? ??? ??? ??? ?num.set(1);
?? ??? ??? ??? ?System.out.println("一號(hào)線程中ThreadLocal變量中保存的值為:" + num.get());
?? ??? ??? ?}
?? ??? ?}).start();
?
?? ??? ?// 創(chuàng)建二號(hào)線程
?? ??? ?new Thread(new Runnable() {
?? ??? ??? ?@Override
?? ??? ??? ?public void run() {
?? ??? ??? ??? ?num.set(2);
?? ??? ??? ??? ?System.out.println("二號(hào)線程中ThreadLocal變量中保存的值為:" + num.get());
?? ??? ??? ?}
?? ??? ?}).start();
?
?? ??? ?//為了讓一二號(hào)線程執(zhí)行完畢,讓主線程睡500ms
?? ??? ?try {
?? ??? ??? ?Thread.sleep(500);
?? ??? ?} catch (InterruptedException e) {
?? ??? ??? ?// TODO Auto-generated catch block
?? ??? ??? ?e.printStackTrace();
?? ??? ?}
?? ??? ?
?? ??? ?System.out.println("主線程中ThreadLocal變量中保存的值:" + num.get());
?? ?}
}
上面的代碼中在類中創(chuàng)建了一個(gè)靜態(tài)的“ThreadLocal變量”,在主線程中創(chuàng)建兩個(gè)線程,在這兩個(gè)線程中分別設(shè)置ThreadLocal變量為1和2。然后等待一號(hào)和二號(hào)線程執(zhí)行完畢后,在主線程中查看ThreadLocal變量的值。
程序結(jié)果及分析:
程序結(jié)果重點(diǎn)看的是主線程輸出的是0,如果是一個(gè)普通變量,在一號(hào)線程和二號(hào)線程中將普通變量設(shè)置為1和2,那么在一二號(hào)線程執(zhí)行完畢后在打印這個(gè)變量,輸出的值肯定是1或者2(到底輸出哪一個(gè)由操作系統(tǒng)的線程調(diào)度邏輯有關(guān))。但使用ThreadLocal變量通過兩個(gè)線程賦值后,在主線程程中輸出的卻是初始值0。在這也就是為什么“一個(gè)ThreadLocal在一個(gè)線程中是共享的,在不同線程之間又是隔離的”,每個(gè)線程都只能看到自己線程的值,這也就是ThreadLocal的核心作用:實(shí)現(xiàn)線程范圍的局部變量。
我們還是將最后結(jié)論擺在前面,每個(gè)Thread對(duì)象都有一個(gè)ThreadLocalMap,當(dāng)創(chuàng)建一個(gè)ThreadLocal的時(shí)候,就會(huì)將該ThreadLocal對(duì)象添加到該Map中,其中鍵就是ThreadLocal,值可以是任意類型。也就是說,想要存入的ThreadLocal中的數(shù)據(jù)實(shí)際上并沒有存到ThreadLocal對(duì)象中去,而是以這個(gè)ThreadLocal實(shí)例作為key存到了當(dāng)前線程中的一個(gè)Map中去了,獲取ThreadLocal的值時(shí)同樣也是這個(gè)道理。這也就是為什么ThreadLocal可以實(shí)現(xiàn)線程之間隔離的原因了。
總結(jié):
1)ThreadLocal的作用:實(shí)現(xiàn)線程范圍內(nèi)的局部變量,即ThreadLocal在一個(gè)線程中是共享的,在不同線程之間是隔離的。
2)ThreadLocal的原理:ThreadLocal存入值時(shí)使用當(dāng)前ThreadLocal實(shí)例作為key,存入當(dāng)前線程對(duì)象中的Map中去。最開始在看源碼之前,我以為是以當(dāng)前線程對(duì)象作為key將對(duì)象存入到ThreadLocal中的Map中去。
兩者都用于解決多線程并發(fā)訪問。但是ThreadLocal與synchronized有本質(zhì)的區(qū)別。Synchronized用于線程間的數(shù)據(jù)共享,而ThreadLocal則用于線程間的數(shù)據(jù)隔離。Synchronized是利用鎖的機(jī)制,使變量或代碼塊在某一時(shí)該只能被一個(gè)線程訪問。而ThreadLocal為每一個(gè)線程都提供了變量的副本,使得每個(gè)線程在某一時(shí)間訪問到的并不是同一個(gè)對(duì)象,這樣就隔離了多個(gè)線程對(duì)數(shù)據(jù)的數(shù)據(jù)共享。而Synchronized卻正好相反,它用于在多個(gè)線程間通信時(shí)能夠獲得數(shù)據(jù)共享。
如果ThreadLocal沒有外部強(qiáng)引用,那么在發(fā)生垃圾回收的時(shí)候,ThreadLocal就必定會(huì)被回收,而ThreadLocal又作為Map中的key,ThreadLocal被回收就會(huì)導(dǎo)致一個(gè)key為null的entry,外部就無(wú)法通過key來(lái)訪問這個(gè)entry,垃圾回收也無(wú)法回收,這就造成了內(nèi)存泄漏
解決方案:每次使用完ThreadLocal都調(diào)用它的remove()方法清除數(shù)據(jù),或者按照J(rèn)DK建議將ThreadLocal變量定義成private static,這樣就一直存在ThreadLocal的強(qiáng)引用,也就能保證任何時(shí)候都能通過ThreadLocal的弱引用訪問到Entry的value值,進(jìn)而清除掉。