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