我們現(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)。