我們現在所使用操作系統都是多任務操作系統(早期使用的DOS操作系統為單任務操作系統),多任務操作指在同一時刻可以同時做多件事(可以同時執行多個程序)。
多進程:每個程序都是一個進程,在操作系統中可以同時執行多個程序,多進程的目的是為了有效的使用CPU資源,每開一個進程系統要為該進程分配相關的系統資源(內存資源);
多線程:線程是進程內部比進程更小的執行單元(執行流|程序片段),每個線程完成一個任務,每個進程內部包含了多個線程每個線程做自己的事情,在進程中的所有線程共享該進程的資源;
主線程:在進程中至少存在一個主線程,其他子線程都由主線程開啟,主線程不一定在其他線程結束后結束,有可能在其他線程結束前結束。Java中的主線程是main線程,是Java的main函數;
當應用場景為計算密集型時:為了將每個cpu充分利用起來,線程數量正常是cpu核數+1,還可以看jdk的使用版本,1.8版本中可以使用cpu核數*2。
當應用場景為io密集型時:做web端開發的時候,涉及到大量的網絡傳輸,不進入持,緩存和與數據庫交互也會存在大量io,當發生io時候,線程就會停止,等待io結束,數據準備好,線程才會繼續執行,所以當io密集時,可以多創建點線程,讓線程等待時候,其他線程執行,更高效的利用cpu效率,他有一個計算公式,套用公式的話,雙核cpu理想的線程數就是20。
采用多線程技術的應用程序可以更好地利用系統資源。主要優勢在于充分利用了CPU的空閑時間片,用盡可能少的時間來對用戶的要求做出響應,使得進程的整體運行效率得到較大提高,同時增強了應用程序的靈活性。由于同一進程的所有線程是共享同一內存,所以不需要特殊的數據傳送機制,不需要建立共享存儲區或共享文件,從而使得不同任務之間的協調操作與運行、數據的交互、資源的分配等問題更加易于解決。
多線程的目的就是為了能提高程序的執行效率提高程序運行速度,但是并發編程并不總是能提高程序運行速度的,而且并發編程可能會遇到很多問題,比如:內存泄漏、死鎖、線程不安全等等。
進程:是正在運行中的程序,是系統進行資源調度和分配的的基本單位。
線程:是進程的子任務,是任務調度和執行的基本單位;
一個程序至少有一個進程,一個進程至少有一個線程,線程依賴于進程而存在;
進程在執行過程中擁有獨立的內存單元,而多個線程共享進程的內存。線程與進程相似,但線程是一個比進程更小的執行單位。
一個進程在其執行的過程中可以產生多個線程。與進程不同的是同類的多個線程共享進程的堆和方法區資源,但每個線程有自己的程序計數器、虛擬機棧和本地方法棧,所以系統在產生一個線程,或是在各個線程之間作切換工作時,負擔要比進程小得多,也正因為如此,線程也被稱為輕量級進程。
a.繼承 Thread 類;b.實現 Runnable 接口;c. 實現Callable接口;d. 使用線程池。
我們可以通過繼承Thread類或者調用Runnable接口來實現線程,因為Java不支持類的多重繼承,但允許你調用多個接口。所以如果你想要繼承其他的類,當然是調用Runnable接口好了。
如果想讓線程池執行任務的話需要實現的Runnable接口或Callable接口。 Runnable接口或Callable接口實現類都可以被ThreadPoolExecutor或ScheduledThreadPoolExecutor執行。兩者的區別在于 Runnable 接口不會返回結果但是 Callable 接口可以返回結果。
啟動一個線程需要調用 Thread 對象的 start() 方法;
調用線程的 start() 方法后,線程處于可運行狀態,此時它可以由 JVM 調度并執行,這并不意味著線程就會立即運行;
run() 方法是線程運行時由 JVM 回調的方法,無需手動寫代碼調用;
直接調用線程的 run() 方法,相當于在調用線程里繼續調用了一個普通的方法,并未啟動一個新的線程。
線程通常有五種狀態:創建,就緒,運行,阻塞和死亡狀態
(1)創建狀態(New):新創建了一個線程對象。
(2)就緒狀態(Runnable):線程對象創建后,其他線程調用了該對象的start()方法。該狀態的線程位于可運行線程池中,變得可運行,等待獲取CPU的使用權。
(3)運行狀態(Running):就緒狀態的線程獲取了CPU,執行程序代碼。
(4)阻塞狀態(Blocked):阻塞狀態是線程因為某種原因放棄CPU使用權,暫時停止運行。直到線程進入就緒狀態,才有機會轉到運行狀態。阻塞的情況分三種:
(一)等待阻塞:運行的線程執行wait()方法,JVM會把該線程放入等待池中。(wait會釋放持有的鎖)
(二)同步阻塞:運行的線程在獲取對象的同步鎖時,若該同步鎖被別的線程占用,則JVM會把該線程放入鎖池中。
(三)其他阻塞:運行的線程執行sleep()或join()方法,或者發出了I/O請求時,JVM會把該線程置為阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程重新轉入就緒狀態。(注意,sleep是不會釋放持有的鎖)
(5)死亡狀態(Dead):線程執行完了或者因異常退出了run()方法,該線程結束生命周期。
多線程編程中一般線程的個數都大于 CPU 核心的個數,而一個 CPU 核心在任意時刻只能被一個線程使用,為了讓這些線程都能得到有效執行,CPU 采取的策略是為每個線程分配時間片并輪轉的形式。當一個線程的時間片用完的時候就會重新處于就緒狀態讓給其他線程使用,這個過程就屬于一次上下文切換。
概括來說就是:當前任務在執行完 CPU 時間片切換到另一個任務之前會先保存自己的狀態,以便下次再切換回這個任務時,可以再加載這個任務的狀態。任務從保存到再加載的過程就是一次上下文切換。
上下文切換通常是計算密集型的。也就是說,它需要相當可觀的處理器時間,在每秒幾十上百次的切換中,每次切換都需要納秒量級的時間。所以,上下文切換對系統來說意味著消耗大量的 CPU 時間,事實上,可能是操作系統中時間消耗最大的操作。
Linux 相比與其他操作系統(包括其他類 Unix 系統)有很多的優點,其中有一項就是,其上下文切換和模式切換的時間消耗非常少。
使用Thread類的setDaemon(true)方法可以將線程設置為守護線程,需要注意的是,需要在調用start()方法前調用這個方法,否則會拋出IllegalThreadStateException異常。
當我們在Java程序中創建一個線程,它就被稱為用戶線程。一個守護線程是在后臺執行并且不會阻止JVM終止的線程。當沒有用戶線程在運行的時候,JVM關閉程序并且退出。一個守護線程創建的子線程依然是守護線程。
Thread類的sleep()和yield()方法將在當前正在執行的線程上運行。所以在其他處于等待狀態的線程上調用這些方法是沒有意義的。這就是為什么這些方法是靜態的。它們可以在當前正在執行的線程中工作,并避免程序員錯誤的認為可以在其他非運行線程調用這些方法。
Windows系統下執行java -jar arthas-boot.jar
Linux系統下解壓arthas,執行ps -ef | grep java找出java進程pid數字
new 一個 Thread,線程進入了新建狀態;調用 start() 方法,會啟動一個線程并使線程進入了就緒狀態,當分配到時間片后就可以開始運行了。 start() 會執行線程的相應準備工作,然后自動執行 run() 方法的內容,這是真正的多線程工作。 而直接執行 run() 方法,會把 run 方法當成一個 main 線程下的普通方法去執行,并不會在某個線程中執行它,所以這并不是多線程工作。
總結: 調用 start 方法方可啟動線程并使線程進入就緒狀態,而 run 方法只是 thread 的一個普通方法調用,還是在主線程里執行.
Callable接口類似于Runnable,從名字就可以看出來,但是Runnable不會返回結果,并且無法拋出返回結果的異常,而Callable功能更強大一些,被線程執行后,可以返回值,這個返回值可以被Future拿到,也就是說,Future可以拿到異步執行任務的返回值。可以認為是帶有返回值的Runnable.Future接口表示異步任務,是還沒有完成的任務給出的未來結果。所以說Callable用于產生結果,Future用于獲取結果。
(1) 搶占式調度策略
Java運行時系統的線程調度算法是搶占式的 (preemptive)。Java運行時系統支持一種簡單的固定優先級的調度算法。如果一個優先級比其他任何處于可運行狀態的線程都高的線程進入就緒狀態,那么運行時系統就會選擇該線程運行。新的優先級較高的線程搶占(preempt)了其他線程。但是Java運行時系統并不搶占同優先級的線程。換句話說,Java運行時系統不是分時的(time-slice)。然而,基于Java Thread類的實現系統可能是支持分時的,因此編寫代碼時不要依賴分時。當系統中的處于就緒狀態的線程都具有相同優先級時,線程調度程序采用一種簡單的、非搶占式的輪轉的調度順序。
(2) 時間片輪轉調度策略
有些系統的線程調度采用時間片輪轉(round-robin)調度策略。這種調度策略是從所有處于就緒狀態的線程中選擇優先級最高的線程分配一定的CPU時間運行。該時間過后再選擇其他線程運行。只有當線程運行結束、放棄(yield)CPU或由于某種原因進入阻塞狀態,低優先級的線程才有機會執行。如果有兩個優先級相同的線程都在等待CPU,則調度程序以輪轉的方式選擇運行的線程。
搶占式。一個線程用完CPU之后,操作系統會根據線程優先級、線程饑餓情況等數據算出一個總的優先級并分配下一個時間片給某個線程執行。
線程調度器是一個操作系統服務,它負責為 Runnable 狀態的線程分配 CPU 時間。一旦我們創建一個線程并啟動它,它的執行便依賴于線程調度器的實現。同上一個問題,線程調度并不受到 Java 虛擬機控制,所以由應用程序來控制它是 更好的選擇(也就是說不要讓你的程序依賴于線程的優先級)。時間分片是指將可用的 CPU 時間分配給可用的 Runnable 線程的過程。分配 CPU 時間可以基于線程優先級或者線程等待的時間。
兩者最主要的區別在于:sleep 方法沒有釋放鎖,而 wait 方法釋放了鎖 。
兩者都可以暫停線程的執行。
wait 通常被用于線程間交互/通信,sleep 通常被用于暫停執行。
wait() 方法被調用后,線程不會自動蘇醒,需要別的線程調用同一個對象上的 notify() 或者 notifyAll() 方法。sleep() 方法執行完成后,線程會自動蘇醒。或者可以使用 wait(long timeout)超時后線程會自動蘇醒。
因為這些方法的調用是依賴鎖對象,而同步代碼塊的鎖對象是任意。鎖而Object代表任意的對象,所以定義在這里面。
使當前線程從執行狀態(運行狀態)變為可執行態(就緒狀態)。當前線程到了就緒狀態,那么接下來具體是哪個個線程會從就緒狀態變成執行狀態就要看系統的分配了。
Thread類的sleep()和yield()方法將在當前正在執行的線程上運行。所以在其他處于等待狀態的線程上調用這些方法是沒有意義的。這就是為什么這些方法是靜態的。它們可以在當前正在執行的線程中工作,并避免程序員錯誤的認為可以在其他非運行線程調用這些方法。
Thread類的sleep()和yield()方法將在當前正在執行的線程上運行。所以在其他處于等待狀態的線程上調用這些方法是沒有意義的。這就是為什么這些方法是靜態的。它們可以在當前正在執行的線程中工作,并避免程序員錯誤的認為可以在其他非運行線程調用這些方法。
線程在運行過程中,有些時候可能需要中斷一些阻塞的線程,類Thread中提供了幾種中斷線程的方法,其中Thread.suspend()和Thread.stop()方法已經過時了,因為這兩個方法是不安全的。Thread.stop(),會直接終止該線程,并且會立即釋放這個線程持有的所有鎖,而這些鎖恰恰是用來維持數據的一致性的,如果此時。寫線程寫入數據時寫到一半,并強行終止,由于此時對象鎖已經被釋放,另一個等待該鎖的讀線程就會讀到這個不一致的對象。Thread.suspend()會導致死鎖,Thread.resume()也不能使用。
Java 提供了很豐富的API 但沒有為停止線程提供 API。JDK 1.0 本來有一些像stop(), suspend() 和 resume()的控制方法但是由于潛在的死鎖威脅因此在后續的JDK 版本中他們被棄用了.之后Java API 的設計者就沒有提供一個兼容且線程安全的方法來停止一個線程。當run() 或者 call() 方法執行完的時候線程會自動結束, 如果要手動結束一個線程.你可以用volatile 布爾變量來退出 run()方法的循環或者是取消任務來中斷線程。
interrupted:查詢當前線程的中斷狀態,并且清除原狀態。如果一個線程被中斷了,第一次調用 interrupted 則返回 true,第二次和后面的就返回 false 了。
isInterrupted僅僅是查詢當前線程的中斷狀態。
1)notify只會隨機選取一個處于等待池中的線程進入鎖池去競爭獲取鎖的機會;
2)notifyAll會讓所有處于等待池的線程全部進入鎖池去競爭獲取鎖的機會;
每一個線程都是有優先級的.一般來說.高優先級的線程在運行時會具有優先權. 但這依賴于線程調度的實現.這個實現是和操作系統相關的(OS dependent)。我們可以定義線程的優先級.但是這并不能保證高優先級的線程會在低優先級的線程前執行。線程優先級是一個int 變量(從 1-10).1 代表最低優先級.10 代表最高優先級。
線程類的構造方法、靜態塊是被new這個線程類所在的線程所調用的,而run方法里面的代碼才是被線程自身所調用的。
舉個例子,假設Thread2中new了Thread1,main函數中new了Thread2,那么:
(1)Thread2的構造方法、靜態塊是main線程調用的,Thread2的run()方法是Thread2自己調用的;
(2)Thread1的構造方法、靜態塊是Thread2調用的,Thread1的run()方法是Thread1自己調用的;
簡單的說,如果異常沒有被捕獲該線程將會停止執行。Thread.UncaughtExceptionHandler 是用于處理未捕獲異常造成線程突然中斷情況的一個內嵌接口。當一個未捕獲異常將造成線程中斷的時候JVM 會使用Thread.getUncaughtExceptionHandler()來查詢線程的UncaughtExceptionHandler 并將線程和異常作為參數傳遞給handler 的uncaughtException()方法進行處理。
(1)線程的生命周期開銷非常高
(2)消耗過多的CPU 資源
如果可運行的線程數量多于可用處理器的數量,那么有線程將會被閑置。大量空閑的線程會占用許多內存,給垃圾回收器帶來壓力,而且大量的線程在競爭CPU 資源時還將產生其他性能的開銷。
(3)降低穩定性
JVM 在可創建線程的數量上存在一個限制,這個限制值將隨著平臺的不同而不同,并且承受著多個因素制約,包括JVM 的啟動參數、Thread 構造函數中請求棧的大小,以及底層操作系統對線程的限制等。如果破壞了這些限制,那么可能拋出OutOfMemoryError 異常。
wait();(強迫一個線程等待)
notify();(通知一個線程繼續執行),
notifyAll()(所有線程繼續執行),
sleep()(強迫一個線程睡眠N毫秒),
join()(等待線程終止)
yield()(線程讓步)等等;
FutureTask 表示一個異步運算的任務。 FutureTask 里面可以傳入一個 Callable 的具體實現類,可以對這
個異步運算的任務的結果進行等待獲取、判斷是否已經完成、取消任務等操作。當然,由于 FutureTask
也是 Runnable 接口的實現類,所以 FutureTask 也可以放入線程池中。
多個線程在正常情況下的運行是互不干擾的,但是CUP對線程的切換是隨機的,這樣線程運行的過程就脫離了我們的控制,如果我們想讓多個線程之間有規律的運行,就需要線程通訊,線程之間通信的可以讓多個線程按照我們預期的運行過程去執行。
1)wait()和notify()
wait(): 當前線程釋放鎖并且進入等待狀態。
notify(): 喚醒當前線程,上面wait() 的時候線程進入了等待狀態,如果我們想讓線程執行需要通過notify()喚醒該線程。
notifyAll(): 喚醒所有進入等待狀態的線程。
2)join()方法
join()方法的作用是使A線程加入B線程中執行,B線程進入阻塞狀態,只有當A線程運行結束后B線程才會繼續執行。
3)volatile關鍵字
volatile 關鍵字是實現線程變量之間真正共享的,就是我們理想中的共享狀態,多個線程同時監控著共享變量,當變量發生變化時其它線程立即改變,具體實現與JMM內存模型有關。