猜大家都很了解線程的使用了,現在我們以java為例,來看看線程是怎樣在底層(jvm里面)產生和運行的。
線程控制模塊:
當我們構造一個線程,java虛擬機會在內存中生成一個線程控制塊,其中包括PC寄存器、Java棧、本地方法棧,這是每個線程獨自擁有的,互不干涉。
PC計數器存放當前正在被執行的字節碼指令(JVM指令)的地址。說白了,就是PC計數器用來記住這個線程被執行到那一步了(方便下次繼續執行)。
Java棧:這個棧中存放着一系列的棧幀(Stack Frame),JVM只能進行壓入(Push)和彈出(Pop)棧幀這兩種操作。每當調用一個方法時,JVM就往棧里壓入一個棧幀,方法結束返回時彈出棧幀。每個棧幀包含三個部分:本地變量數組、操作數棧(操作數棧中存放方法執行時的一些中間變量,JVM在執行方法時壓入或者彈出這些變量。其實,操作數棧是方法真正工作的地方,其中我們定義的各種基礎數據類型的變量,和對象的引用變量都在操作數棧的內存中儲存。當一個函數執行完后,它對棧內存的占用也會被釋放,供下一個函數使用)、方法所屬類的常量池引用。
本地方法棧:這個棧用來存放本地語言(如C或者C++代碼)的方法調用信息,我們知道java是通過在操作系統的基礎上虛擬出一層環境(稱為JRE)來運行我們的java 程序的,編寫操作系統的語言多數時候並不是java(例如windows和linux都是C語言編寫的),當程序通過JNI(Java Native Interface)調用本地方法(如C或者C++代碼)時,就根據本地方法的語言類型建立相應的棧。
線程的數據共享方式:
所有線程公用的數據區域:Java堆、方法區域、運行常量池。
Java堆中儲存的是java中所有被創建出來的對象。操作數棧中的對象的引用(這個類似於C++中的指針),就是指向java堆的。
方法區域是一個JVM實例中的所有線程共享的,當啟動一個JVM實例時,方法區域被創建。它用於存運行放常量池、有關域和方法的信息、靜態變量、類和方法的字節碼。我們在運行java程序之前,jvm會先查找並且加載有關的類,被加載的類就被儲存在這里。
運行常量池:這個區域存放類和接口的常量,除此之外,它還存放方法和域的所有引用。當一個方法或者域被引用的時候,JVM就通過運行常量池中的這些引用來查找方法和域在內存中的的實際地址。
線程的各種狀態:
1、新建狀態(New):新創建了一個線程對象。
2、就緒狀態(Runnable):線程對象創建后,其他線程調用了該對象的start()方法。該狀態的線程位於“可運行線程池”中,變得可運行,只等待獲取CPU的使用權。即在就緒狀態的進程除CPU之外,其它的運行所需資源都已全部獲得。
3、運行狀態(Running):就緒狀態的線程獲取了CPU,執行程序代碼。
4、暫停運行狀態(Blocked):暫停運行狀態是線程因為某種原因放棄CPU使用權,暫時停止運行。直到線程進入就緒狀態,才有機會轉到運行狀態。
暫停運行狀態的情況分三種:
(1)等待:運行的線程執行wait()或join()方法,該線程會釋放占用的所有資源,JVM會把該線程放入“等待池”中。進入這個狀態后,是不能自動喚醒的,必須依靠其他線程調用notify()或notifyAll()方法才能被喚醒,join()等待線程終止或者超時、或者I/O處理完畢時,線程重新轉入就緒狀態。
(2)阻塞:運行的線程在獲取對象的同步鎖時,若該同步鎖被別的線程占用,則JVM會把該線程放入“鎖池”中。
(3)睡眠:運行的線程執行sleep()方法,或者發出了I/O請求時,JVM會把該線程置為阻塞狀態。當sleep()狀態超時,線程重新轉入就緒狀態。(網上有部分文章也會把join()和sleep()歸為一類,這里為了描述特性方便,把wait()和join()歸為一類)。
5、死亡狀態(Dead):線程執行完了或者因異常退出了run()方法,該線程結束生命周期。
線程的各種狀態如圖所示,已經非常明了,這里不再累述。
線程的各種操作:
線程的掛起(又稱為等待),和進程相類似,線程在運行中也同樣會有掛起的狀態,掛起是指進程控制模塊,在設備資源(比如說cup計算資源,內存資源)快要用盡的時候,為了節省設備資源,把一部分不太重要的線程從內存轉移到外存去(這時候,這些線程就不能運行了)。
1.被線程A占用的對象H,可以發出請求,使線程A進去掛起狀態,在java中,可以使用對象中的object類下的wait(),(因為所有類都繼承自object類,所以任何對象都有此方法,也就是說java中的任何一個對象都自帶一個同步鎖) 線程A在進入等待以后,需要notify()方法才能被喚醒,才能繼續執行。
2.可以使用Thread類下的join()方法,以及等待Lock或Condition。這一實現方法會讓被等待的線程一直被掛起,直到調用這一方法的線程執行完畢(線程死亡),例如:線程A運行Thread.join(),掛起了線程B,這時候,這時候線程B必須在A的run()函數完全執行完以后,才能再次開始執行。
阻塞:
1.在某一個線程嘗試獲取某一個資源(比如一個對象的對象鎖),而該鎖被其他線程持有,這個線程暫時沒有辦法繼續執行,所以CPU不再分配時間片給它(但是它的內存並不釋放),來加速程序運行。
掛起和阻塞的比較:
掛起一般是程序的主動行為,由系統或程序發出,甚至於輔存中去。(不釋放CPU,可能釋放內存,放在外存);阻塞一般是被動的,在搶占資源中得不到資源,被動的掛起在內存,等待某種資源或信號量(即有了資源)將他喚醒。(釋放CPU,不釋放內存)。
線程的掛起和釋放,需要顯式調用wait和notify方法;阻塞和釋放則是java的虛擬機自動完成的。不需要程序員去處理
睡眠:
睡眠是線程主動請求暫停執行一段時間(這段時間里,進入阻塞狀態的,釋放CPU,不釋放內存),多數時候線程之所以加入這個行為,主要是為了減輕當前線程對CPU的負荷,
讓步:
例如java 中的yield(),它也可以讓當前執行的線程暫停,但它不會阻塞線程,只是將該線程轉入到就緒狀態。 yield()只是讓當前線程暫停下,讓系統線程調度器重新調度下。系統線程調度器會讓優先級相同或是更高的線程運行。
死亡(中斷):
死亡(中斷)以為着線程的結束,這個線程出現了致命異常,無法繼續執行,然后線程會自動死亡。
程序中睡眠、阻塞、掛起的區別形象解釋:
首先這些術語都是對於線程來說的。對線程的控制就好比你控制了一個雇工為你干活。你對雇工的控制是通過編程來實現的。
掛起線程的意思就是你對主動對雇工說:“你睡覺去吧,用着你的時候我主動去叫你,然后接着干活”。
使線程睡眠的意思就是你主動對雇工說:“你睡覺去吧,某時某刻過來報到,然后接着干活”。
線程阻塞的意思就是,你突然發現,你的雇工不知道在什么時候沒經過你允許,自己睡覺呢,但是你不能怪雇工,肯定你這個雇主沒注意,本來你讓雇工掃地,結果掃帚被偷了或被鄰居家借去了,你又沒讓雇工繼續干別的活,他就只好睡覺了。至於掃帚回來后,雇工會不會知道,會不會繼續干活,你不用擔心,雇工一旦發現掃帚回來了,他就會自己去干活的。因為雇工受過良好的培訓。這個培訓機構就是操作系統。
針對對象而言:
對象在線程中的應用除了同步鎖以外,還有一種應用——對象object中的這個synchronized(this)同步代碼塊,這種代碼塊類似於 python中的lock的使用方法:
Java語言的關鍵字,當它用來修飾一個方法或者一個代碼塊的時候,能夠保證在同一時刻最多只有一個線程執行該段代碼。
一、當兩個並發線程訪問同一個對象object中的這個synchronized(this)同步代碼塊時,一個時間內只能有一個線程得到執行。另一個線程必須等待當前線程執行完這個代碼塊以后才能執行該代碼塊。
二、然而,當一個線程訪問object的一個synchronized(this)同步代碼塊時,另一個線程仍然可以訪問該object中的非synchronized(this)同步代碼塊。
三、尤其關鍵的是,當一個線程訪問object的一個synchronized(this)同步代碼塊時,其他線程對object中所有其它synchronized(this)同步代碼塊的訪問將被阻塞。
四、第三個例子同樣適用其它同步代碼塊。也就是說,當一個線程訪問object的一個synchronized(this)同步代碼塊時,它就獲得了這個object的對象鎖。結果,其它線程對該object對象所有同步代碼部分的訪問都被暫時阻塞。
簡單的說,synchronized(this)同步代碼塊內的內容被視為一個整體,java的一個線程在執行代碼塊的過程中,必須執行完同步代碼塊內的所有內容(或遇到obj.wait()被掛起),才可以釋放對象鎖。
綜上:
某個對象的池鎖里的線程若獲得對象鎖,那么會繼續執行wait()之后的代碼,要想其釋放對象鎖,只有以下幾種情況:
1. 執行完同步代碼塊。
2. 線程終止。
3. 對象的wait()方法被調用。
線程的執行順序:
操作程序運行過程中,每個線程都會希望獲得CPU的控制權,那么它們執行的先后順序要怎么排列呢?怎么保證同級別的線程中JVM不會“厚此薄彼”呢?又怎么保證重要的線程能獲得更多的執行時間呢?
這其中有很多種已經比較成熟調度線程的算法:例如先來先服務和短作業優先算法,高優先權調度算法,時間片輪轉調度算法。
這里以JVM為例介紹一種控制線程的方法:高優先權調度算法。
先來熟悉一個概念:線程隊列
JVM會把一個程序中所有的線程按照啟動順序先后分配內存,並且創建線程控制模塊(參看文章開頭),然后會給每一個線程創建一個引用(其作用類似於指針),並存入一個設置好的隊列(一種數據結構,特點是先入先出。可參看《數據結構與算法》),於是乎所有的線程都會有序的去排隊等待獲得CPU的運行時間片(如果沒有特殊設置,每個線程獲得的時間片是相等的)。如果一個線程在自己擁有的時間片中完成(即run()方法執行結束。線程死亡)則撤出JVM,同時釋放線程控制模塊;如果沒有完成,則這個線程會被從新放入隊列的尾部,等待再次被調用。
高優先權調度算法就是基於這種“排隊,平均分配”的思想來調度線程的,但是為了保證短作業可以優先完成,重要作業可以優先被執行的實際需要。
高優先權調度算法實現如下:
如上圖,按照優先級,會分別建立多個隊列,每個線程按照先來先排隊的原則分別進入相應優先級的隊列,然后CPU會先執行優先級最高的隊列,然后依次類推(執行順序如圖中的數字序號)圖中當1號線程在自己的時間片中沒有執行完成時,它會被放入優先級3的隊列的末尾,等待被再次執行,如果下次執行還是沒有完成,則會被放入優先級2的隊列的末尾,(其他線程也以此類推)。
在時間片的分配上:低優先級的線程的時間片會比高優先級的時間片長(對,你沒有聽錯)。例如,優先級2的隊列,時間片長度是優先級3的兩倍,是優先級4的四倍(以此類推)。這是為了保證,高優先級的和短作業的任務會被優先執行。
嗯,本次就先分享這么多吧,如果本文有錯誤疏漏可以在下方留言指正,作者QQ:823811845。