清華掃地僧整理的全網最全多線程詳解,看完懷疑自己的認知 前言 一、線程概述 二、線程與進程 三、使用多線程 四、控制線程 五、線程同步 六、線程池 後記

前言

現在越來越多的公司,對精通多線程的的人才越來越重視,可見多線程技術有多熱門。今天,小編結合清華掃地僧級別大佬的分享,爲大家帶來這篇多線程的總結,希望大家能夠喜歡。

一、線程概述

幾乎所有的操作系統都支持同時運行多個任務,一個任務通常就是一個程序,每個運行中的程序就是一個進程。當一個程序運行時,內部可能包含了多個順序執行流,每個順序執行流就是一個線程。

二、線程與進程

進程概述:

幾乎所有的操作系統都支持進程的概念,所有運行中的任務通常對應一個進程( Process)。當一個程序進入內存運行時,即變成一個進程。進程是處於運行過程中的程序,並且具有一定的獨立功能,進程是系統進行資源分配和調度的一個獨立單位。

進程特徵:

1、獨立性:進程是系統中獨立存在的實體,它可以擁有自己獨立的資源,每一個進程都擁有自己私有的地址空間。在沒有經過進程本身允許的情況下,一個用戶進程不可以直接訪問其他進程的地址空間

2、動態性:進程與程序的區別在於,程序只是一個靜態的指令集合,而進程是一個正在系統中活動的指令集合。在進程中加入了時間的概念。進程具有自己的生命週期和各種不同的狀態,這些概念在程序中都是不具備的

3、併發性:多個進程可以在單個處理器上併發執行,多個進程之間不會互相影響。

線程:

線程與進程相似,但線程是一個比進程更小的執行單位。一個進程在其執行的過程中可以產生多個線程。與進程不同的是同類的多個線程共享同一塊內存空間和一組系統資源,所以系統在產生一個線程,或是在各個線程之間作切換工作時,負擔要比進程小得多,也正因爲如此,線程也被稱爲輕量級進程。

併發和並行:

併發:同一時刻只能有一條指令執行,但多個進程指令被快速輪換執行

並行:同一時刻,有多條指令在多個處理器上同時執行

多線程:

概述:

多線程就是幾乎同時執行多個線程(一個處理器在某一個時間點上永遠都只能是一個線程!即使這個處理器是多核的,除非有多個處理器才能實現多個線程同時運行。)。幾乎同時是因爲實際上多線程程序中的多個線程實際上是一個線程執行一會然後其他的線程再執行,並不是很多書籍所謂的同時執行。

多線程優點:

進程之間不能共享內存,但線程之間共享內存非常容易。

系統創建進程時需要爲該進程重新分配系統資源,但創建線程則代價小得多,因此使用多線程來實現多任務併發比多進程的效率高

Java語言內置了多線程功能支持,而不是單純地作爲底層操作系統的調度方式,從而簡化了Java的多線程編程

三、使用多線程

多線程的創建:

(1)、繼承Thread類:

第一步:定義Thread類的之類,並重寫run方法,該run方法的方法體就代表了線程需要執行的任務

第二步:創建Thread類的實例

第三步:調用線程的start()方法來啓動線程

(2)、實現Runnable接口:

第一步:定義Runnable接口的實現類,並重寫該接口的run方法,該run方法同樣是線程需要執行的任務

第二步:創建Runnable實現類的實例,並以此實例作爲Thread的target來創建Thread對象,該Thread對象纔是真正的線程對象

(3)、使用Callable和Future創建線程

細心的讀者會發現,上面創建線程的兩種方法。繼承Thread和實現Runnable接口中的run都是沒有返回值的。於是從Java5開始,Java提供了Callable接口,該接口是Runnable接口的增強版。Callable接口提供了一個call()方法可以作爲線程執行體,但call()方法比run()方法功能更強大。

創建並啓動有返回值的線程的步驟如下:

第一步:創建 Callable接口的實現類,並實現call()方法,該call()方法將作爲線程執行體,且該call()方法有返回值,再創建 Callable實現類的實例。從Java8開始,可以直接使用 Lambda表達式創建 Callable對象

第二步:使用FutureTask類來包裝Callable對象,該FutureTask對象封裝了該Callable對象的call方法的返回值

第三步:使用FutureTask對象作爲Thread對象的target創建並啓動新線程

第四步:通過FutureTask的get()方法獲得子線程執行結束後的返回值

創建線程的三種方式的對比:

採用Runnable、Callable接口的方式創建多線程的優缺點:

優點:

1、線程類只是實現了 Runnable接口或 Callable接口,還可以繼承其他類

2、在這種方式下,多個線程可以共享同一個 target對象,所以非常適合多個相同線程來處理同一份資源的情況,從而可以將CPU、代碼和數據分開,形成清晰的模型,較好地體現了面向對象的思想。

缺點:

編程稍稍複雜,如果需要訪問當前線程,則必須使用Thread.currentThread()方法。

採用繼承 Thread類的方式創建多線程的優缺點:

優點:

編寫簡單,如果需要訪問當前線程,則無須使用 Thread.current Thread()方法,直接使用this即可獲得當前線程

缺點:

因爲線程已經繼承了Thread類,所以不能再繼承其他類

線程的生命週期:

新建和就緒狀態:

當程序使用new關鍵字創建一個線程後,該線程就處於新建狀態。

當線程對象調用了start()方法後,該線程就處於就緒狀態。

運行和阻塞狀態:

如果處於就緒狀態的線程獲取了CPU,開始執行run()方法的線程執行體,則該線程處於運行狀態。

當線程調用sleep(),調用一個阻塞式IO方法,線程會被阻塞

死亡狀態:

1、run()或者call()方法執行完成,線程正常結束

2、線程拋出一個未捕獲的Exception或Error

3、直接調用該線程的stop方法來結束該線程——該方法容易導致死鎖,不推薦使用

四、控制線程

join線程

Thread提供了讓一個線程等待另一個線程完成的方法——join方法。當在某個程序執行流中調用其直到被 join方法加入的join線程執行完爲止

運行結果

後臺線程:

有一種線程,它是在後臺運行的,它的任務是爲其他的線程提供服務,這種線程被稱爲“後臺線程( Daemon Thread)”,又稱爲“守護線程”或“精靈線程”。JVM的垃圾回收線程就是典型的後臺線程。

後臺線程有個特徵:如果所有的前臺線程都死亡,後臺線程會自動死亡。

調用 Thread對象的 setDaemon(true)方法可將指定線程設置成後臺線程。下面程序將執行線程設置成後臺線程,可以看到當所有的前臺線程死亡時,後臺線程隨之死亡。當整個虛擬機中只剩下後臺線程時,程序就沒有繼續運行的必要了,所以虛擬機也就退出了。

運行結果:

線程睡眠:

如果需要讓當前正在執行的線程暫停一段時間,並進入阻塞狀態,則可以通過調用 Thread類的靜態 sleep方法來實現。 sleep方法有兩種重載形式

static void sleep(long millis):讓當前正在執行的線程暫停millis毫秒,並進入阻塞狀態

static void sleep(long millis,int nanos):讓當前正在執行的線程暫停millis毫秒加上nanos毫微秒,並進入阻塞狀態,通常我們不會精確到毫微秒,所以該方法不常用

改變線程優先級:

每個線程執行時都有一定的優先級,優先級高的線程獲得較多的執行機會,優先級低的線程則獲得較少的執行機會。

每個線程默認的優先級都與創建它的父線程的優先級相同,在默認情況下,main線程具有普通優先級,由main線程創建的子線程也具有普通優先級。

Thread類提供了 setPriority(int newPriority)、 getPriority()方法來設置和返回指定線程的優先級,其中 setPriority()方法的參數可以是一個整數,範圍是1-10之間,也可以使用 Thread類的如下三個靜態常量

MAX_PRIORITY:其值是10

MIN_PRIORITY:其值時1

NORM_PRIPRITY:其值是5

五、線程同步

線程安全問題:

現有如下代碼:

現在我們來分析一下以上代碼:

我們現在希望實現的操作是模擬多個用戶同時從銀行賬戶裏面取錢,如果用戶取錢數小於等於當前賬戶餘額,則提示取款成功,並將餘額減去取款錢數,如果餘額不足,則提示餘額不足,取款失敗。

Account 類:銀行賬戶類,裏面有一些賬戶的基本信息,以及操作賬戶信息的方法

DrawThread類:繼承了Thread,是一個多線程類,用於模擬多個用戶操作同一個賬戶的信息

DrawTest:測試類

這時我們運行程序可能會看到如下運行結果:

如何解決線程安全問題:

①、同步代碼塊:

爲了解決線程問題,Java的多線程支持引入了同步監視器來解決這個問題,使用同步監視器的通用方法就是同步代碼塊。同步代碼塊的語法格式如下:

synchronized(obj){

  //此處的代碼就是同步代碼塊

}

我們將上面銀行中DrawThread類作如下修改:

我們來看這次的運行結果:

我們發現結果變了,是我們希望看到的結果。因爲我們在可能發生線程安全問題的地方加上了synchronized代碼塊

②:同步方法:

與同步代碼塊對應,Java的多線程安全支持還提供了同步方法,同步方法就是使用 synchronized關鍵字來修飾某個方法,則該方法稱爲同步方法。對於 synchronized修飾的實例方法(非 static方法)而言,無須顯式指定同步監視器,同步方法的同步監視器是this,也就是調用該方法的對象。同步方法語法格式如下:

public synchronized void 方法名(){

  //具體代碼

}

③、同步鎖:

從Java5開始,Java提供了一種功能更強大的線程同步機制—一通過顯式定義同步鎖對象來實現同步,在這種機制下,同步鎖由Lock對象充當。

Lock提供了比 synchronized方法和 synchronized代碼塊更廣泛的鎖定操作,Lock允許實現更靈活的結構,可以具有差別很大的屬性,並且支持多個相關的 Condition對象。

在實現線程安全的控制中,比較常用的是 ReentrantLock(可重入鎖)。使用該Lock對象可以顯式加鎖、釋放鎖,通常使用ReentrantLock的代碼格式如下:

死鎖:

當兩個線程相互等待對方釋放同步監視器時就會發生死鎖,Java虛擬機沒有監測,也沒有采取措施來處理死鎖情況,所以多線程編程時應該採取措施避免死鎖岀現。一旦岀現死鎖,整個程序既不會發生任何異常,也不會給出任何提示,只是所有線程處於阻塞狀態,無法繼續。

死鎖是很容易發生的,尤其在系統中出現多個同步監視器的情況下,如下程序將會出現死鎖

運行結果:

從圖中可以看出,程序既無法向下執行,也不會拋出任何異常,就一直“僵持”着。究其原因,是因爲:上面程序中A對象和B對象的方法都是同步方法,也就是A對象和B對象都是同步鎖。程序中兩個線程執行,副線程的線程執行體是 DeadLock類的run()方法,主線程的線程執行體是 Deadlock的main()方法(主線程調用了init()方法)。其中run()方法中讓B對象調用b進入foo()方法之前,該線程對A對象加鎖—當程序執行到①號代碼時,主線程暫停200ms:CPU切換到執行另一個線程,讓B對象執行bar()方法,所以看到副線程開始執行B實例的bar()方法,進入bar()方法之前,該線程對B對象加鎖——當程序執行到②號代碼時,副線程也暫停200ms:接下來主線程會先醒過來,繼續向下執行,直到③號代碼處希望調用B對象的last()方法——執行該方法之前必須先對B對象加鎖,但此時副線程正保持着B對象的鎖,所以主線程阻塞;接下來副線程應該也醒過來了,繼續向下執行,直到④號代碼處希望調用A對象的 last()方法——執行該方法之前必須先對A對象加鎖,但此時主線程沒有釋放對A對象的鎖——至此,就出現了主線程保持着A對象的鎖,等待對B對象加鎖,而副線程保持着B對象的鎖,等待對A對象加鎖,兩個線程互相等待對方先釋放,所以就出現了死鎖。

六、線程池

系統啓動一個新線程的成本是比較高的,因爲它涉及與操作系統交互。在這種情形下,使用線程池可以很好地提高性能,尤其是當程序中需要創建大量生存期很短暫的線程時,更應該考慮使用線程池。

與數據庫連接池類似的是,線程池在系統啓動時即創建大量空閒的線程,程序將一個 Runnable對象或 Callable對象傳給線程池,線程池就會啓動一個空閒的線程來執行它們的run()或call()方法,當run()或call()方法執行結束後,該線程並不會死亡,而是再次返回線程池中成爲空閒狀態,等待執行下一個Runnable對象的run()或call()方法。

創建線程池的幾個常用的方法:

1.newSingleThreadExecutor創建一個單線程的線程池。這個線程池只有一個線程在工作,也就是相當於單線程串行執行所有任務。如果這個唯一的線程因爲異常結束,那麼會有一個新的線程來替代它。此線程池保證所有任務的執行順序按照任務的提交順序執行。

2.newFixedThreadPool創建固定大小的線程池。每次提交一個任務就創建一個線程,直到線程達到線程池的最大大小。線程池的大小一旦達到最大值就會保持不變,如果某個線程因爲執行異常而結束,那麼線程池會補充一個新線程。

3.newCachedThreadPool創建一個可緩存的線程池。如果線程池的大小超過了處理任務所需要的線程,

那麼就會回收部分空閒(60秒不執行任務)的線程,當任務數增加時,此線程池又可以智能的添加新線程來處理任務。此線程池不會對線程池大小做限制,線程池大小完全依賴於操作系統(或者說JVM)能夠創建的最大線程大小。

4.newScheduledThreadPool創建一個大小無限的線程池。此線程池支持定時以及週期性執行任務的需求。

運行結果:

後記

怎麼樣?看完這些清華大佬整理的多線程筆記,是不是擴充了自己的認知?如果感覺到有幫助,請多多點贊評論轉發,關注小編,你們的支持就是小編最大的動力~~~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章