Java中的執行緒Thread解析及用途
Java中的執行緒
程序和執行緒
在併發性程式中,有兩個基本的執行單元:程序和執行緒。在Java程式語言中,併發程式設計大多數情況下都是和執行緒相關。然而,程序也是很重要的。
一個計算機系統中通常都有很多活動的程序和執行緒。這一點即使是在只有一個執行核心,並且在給定時刻只能執行一個執行緒的系統中都是存在的。單一核心的處理時間是由整個作業系統的“時間片”特性來在眾多的程序和執行緒中共享的。
現在,計算機系統中有多個處理器或者是有多核處理器的情況越來越普遍。這就大大增強了系統執行多個程序和執行緒的併發性。
程序
一個程序就是一個獨立的執行環境。程序有著完整的,私有的基本的執行時資源,尤其是每個程序都有自己的記憶體空間。
程序通常會被看作是程式或者是應用程式的同義詞。然而,使用者看到的應用程式實際上可能是多個相互協作的程序。為了方便程序間通訊,絕大多數的作業系統都支援IPC(Inter Process Communication , 程序間通訊),諸如管道(pipe)和套接字(socket)。 IPC不僅可用於統一系統中程序間的相互通訊,還可以用於不同系統間的程序通訊。
大多數的java虛擬機器的實現都是作為一個單獨的程序的。通過使用ProcessBuilder,Java應用程式可以建立額外的程序。多程序的應用程式超出了本節討論的範圍。
執行緒
執行緒有時被稱為是輕型的程序。程序和執行緒都提供了一種執行環境。但是建立一個新的執行緒比建立一個新的程序需要更少的資源。
執行緒存在於程序之中——每個程序中至少有一個執行緒。同一程序的多個執行緒間共享程序的資源,包括記憶體和檔案。這樣做是出於效率的考慮,但是可能帶來潛在的通訊問題。
多執行緒是Java平臺的一個重要特性。如果我們將進行諸如記憶體管理和訊號處理的執行緒算上的“系統”執行緒計算上的話,那麼每一個應用程式至少都有一個執行緒,或者是多個執行緒。但是從應用程式的程式設計人員的角度來看,我們總是從一個叫做主執行緒的執行緒開始。該執行緒能夠建立其他的執行緒,這點我們會在下一節中進行討論。
執行緒物件
每一個執行緒都是和類Thread的例項相關聯的。在Java中,有兩種基本的使用Thread物件的方式,可用來建立併發性程式。
- 在應用程式需要發起非同步任務的時候,只要生成一個Thread物件即可,這樣可以直接控制執行緒的建立並對其進行管理。
- 把應用程式的任務交給執行器(executor),這樣可以將對執行緒的管理從程式中抽離出來。
本節將對使用Thread物件這種方式進行討論。採用執行器的方式可以參閱高階的併發性物件(high-levelconcurrency objects)。
定義並啟動一個執行緒
建立執行緒的應用程式必須提供執行緒執行時的程式碼。有兩種方式:
- 提供一個可執行的物件(Runnableobject)。介面Runnable中定義了唯一的方法:run, 意思就是說其中含有執行時需要執行的程式碼。該Runnable物件被傳遞給Thread的建構函式,如下:
- public classHelloRunnable implements Runnable {
- publicvoid run() {
- System.out.println("Hello from athread!");
- }
- publicstaticvoid main(String args[]) {
- (new Thread(newHelloRunnable())).start();
- }
- }
- 繼承Thread類。類Thread本身已經實現了Runnable,儘管其實現中什麼也沒有做。應用程式中,可以繼承該類,並提供自己的run實現,如下:
- publicclass HelloThread extends Thread {
- publicvoid run() {
- System.out.println("Hello from a thread!");
- }
- publicstaticvoid main(String args[]) {
- (new HelloThread()).start();
- }
- }<span style="font-weight: bold; "> </span>
—————————————————————————————————————————————————————————————————————————————
注意:上面兩個示例程式中都呼叫了Thread.start來啟動一個新的執行緒。—————————————————————————————————————————————————————————————————————————————
那我們到底應該選用上面兩種方式中的哪種呢?第一種方式更通用一些,因為Runnable物件可以是從Thread類之外的別的類派生而來的。而第二種方式在一些簡單的程式中用起來則更方法;但是這種方式中線性了我們的任務類只能是從Thread派生而來的(可以直接派生,也可以是間接派生)。本書將聚焦於第一種方式,也就是將可執行的任務和執行該任務的Thread物件向分離的這種方式。這不僅僅是因為這種方式更靈活,還由於這種方式更適合我們後面要討論的用於管理執行緒的高階API。
類Thread中定義了一些用於執行緒管理的方法。這其中就有一些是靜態方法。他們可用來獲取呼叫這些方法的執行緒的資訊;或者是用來修改呼叫這些方法的執行緒的狀態。而其他的一些方法則由涉及到對這些執行緒和Thread物件進行管理的執行緒來呼叫。我們將會在接著的小節中對其中的一些方法進行討論。
使用sleep來暫停執行緒的執行
呼叫方法Thread.sleep可以讓當前執行緒掛起一個指定的時間段。這是一種讓程序的其他執行緒得到處理器時間的一個有效手段;或者是系統中其他應用程式得到處理器時間的一個很好方式。該方法還可用協調節奏,正如下面的例項程式中的那樣,可用來等待別的需要一定時間要求來處理其任務的執行緒。
該方法有著兩種過載形式:以毫秒為單位指定睡眠時間和以納秒為單位指定睡眠時間。但是,其中指定的時間並不能保證很精確。這是因為這點取決於作業系統。另外,睡眠是可以通過中斷而結束的,這點我們將會在本小節的後面看到。總之,我們不能見底呼叫該方法就可以確保執行緒被精確地掛起指定的時間長度。
示例程式SleepMessages中使用sleep來實現間隔4秒鐘列印訊息:
- publicclass SleepMessages {
- publicstaticvoid main(String args[])
- throws InterruptedException {
- String importantInfo[] = {
- "Mares eat oats",
- "Does eat oats",
- "Little lambs eat ivy",
- "A kid will eat ivy too"
- };
- for (int i = 0;
- i < importantInfo.length;
- i++) {
- //Pause for 4 seconds
- Thread.sleep(4000);
- //Print a message
- System.out.println(importantInfo[i]);
- }
- }
- }<span style="background-color: rgb(238, 236, 225); "> </span>
—————————————————————————————————————————————————————————————————————————————注意:在main的宣告表明它會丟擲InterruptedException異常。這種異常是當sleep出於啟用態時被別的程式中斷是會丟擲的異常。由於在上面的示例程式中沒有別的執行緒來進行中斷,因此沒有必要捕獲這個異常。
—————————————————————————————————————————————————————————————————————————————
中斷
中斷就是一種指示:執行緒應該停止正在進行的事情。至於執行緒是如何響應這種中斷指示完全是由程式設計師決定的,但是一般情況下都是終止該執行緒。這中使用方法也是在本節內容中所要強調的。
執行緒通過呼叫需要被中斷的執行緒的interrupt方法來發送中斷指示。為了能夠是的中斷機制正常工作,被中斷的執行緒必須支援自身的中斷。
支援中斷
執行緒如何支援自身的中斷了?這取決於它正在做的事情。如果執行緒中頻繁呼叫能夠丟擲InterruptedException異常的方法,那麼在捕獲這種異常後,可以只是簡單地退出執行緒。例如,在SleepMessages示例程式中,迴圈列印訊息的程式碼段是線上程可執行物件的run方法中,那麼可以按照如下方式對其進行修改,以便支援中斷:
- for (int i = 0; i < importantInfo.length; i++) {
- // Pause for 4 seconds
- try {
- Thread.sleep(4000);
- } catch (InterruptedException e) {
- // We've been interrupted: no more messages.
- return;
- }
- // Print a message
- System.out.println(importantInfo[i]);
- }
許多丟擲InterruptedException的方法,例如sleep方法的設計都是在接收到中斷指示的時候立刻中斷當前的操作並返回。
如果一個執行緒長時間執行而沒有呼叫到可以丟擲InterruptedException的異常,又該怎麼樣了?他必須週期性地呼叫Thread.interrupted來檢查是否接收到中斷指示。該方法在接收到中斷指示的時候返回true。例如:
- for (int i = 0; i < inputs.length; i++) {
- heavyCrunch(inputs[i]);
- if (Thread.interrupted()) {
- // We've been interrupted: no more crunching.
- return;
- }
- }
在上面的這個簡單示例程式中,我們僅僅是對中斷指示進行了簡單的檢測,並在檢測到該指示後退出線程。在更為複雜的應用程式中,丟擲這種InterruptedException則顯得根偉合理些。
- if (Thread.interrupted()) {
- thrownew InterruptedException();
- }
中斷指示標記
中斷機制的實現是通過內部的中斷狀態來完成的。呼叫Thread.interrupt會設定該狀態。當一個執行緒通過呼叫靜態的Thread.interrupted方法來檢查另外一個執行緒的中斷狀態時,該狀態會被清除掉。而使用非靜態的isInterrupted方法來檢查一個執行緒的中斷狀態則不會修改狀態的值。
一般的做法是:任何通過丟擲InterruptedException異常而退出的方法都會清除中斷狀態。然而,中斷狀態很快又被別的執行緒通過呼叫interrupt而再次設定也是有可能的。
執行緒的連線(join)
join方法將會使得一個執行緒等待另外一個執行緒執行結束。假如t是目前正在執行的執行緒執行緒物件:
t.join();
將會導致當前執行緒暫停,直到t的執行緒終止。join方法的過載版本使得程式設計師可以指定等待時長。然而,和sleep一樣,這取決於作業系統,因此不能假定join方法的確可以等待那麼長時間。
和sleep類似,join方法可以相應中斷:通過丟擲InterruptedException而退出。
SimpleThreads示例程式
下面的程式中用到了前面提到的部分概念。SimpleThreads程式中含有兩個執行緒。第一個就是每個Java程式都有的main執行緒。在main執行緒中,通過Runnable物件建立了一個執行緒:MessageLoop,並等待期執行完畢。如果MessageLoop執行緒執行的時間過長,main執行緒就會中斷它。
MessageLoop執行緒打印出一系列的資訊。如果在完成資訊列印之前被中斷,該執行緒就會列印一個訊息並退出。
- publicclass SimpleThreads {
- // Display a message, preceded by
- // the name of the current thread
- staticvoid threadMessage(String message) {
- String threadName =
- Thread.currentThread().getName();
- System.out.format("%s: %s%n",
- threadName,
- message);
- }
- privatestaticclass MessageLoop
- implements Runnable {
- publicvoid run() {
- String importantInfo[] = {
- "Mares eat oats",
- "Does eat oats",
- "Little lambs eat ivy",
- "A kid will eat ivy too"
- };
- try {
- for (int i = 0;
- i < importantInfo.length;
- i++) {
- // Pause for 4 seconds
- Thread.sleep(4000);
- // Print a message
- threadMessage(importantInfo[i]);
- }
- } catch (InterruptedException e) {
- threadMessage("I wasn't done!");
- }
- }
- }
- publicstaticvoid main(String args[])
- throws InterruptedException {
- // Delay, in milliseconds before
- // we interrupt MessageLoop
- // thread (default one hour).
- long patience = 1000 * 60 * 60;
- // If command line argument
- // present, gives patience
- // in seconds.
- if (args.length > 0) {
- try {
- patience = Long.parseLong(args[0]) * 1000;
- } catch (NumberFormatException e) {
- System.err.println("Argument must be an integer.");
- System.exit(1);
- }
- }
- threadMessage("Starting MessageLoop thread");
- long startTime = System.currentTimeMillis();
- Thread t = new Thread(new MessageLoop());
- t.start();
- threadMessage("Waiting for MessageLoop thread to finish");
- // loop until MessageLoop
- // thread exits
- while (t.isAlive()) {
- threadMessage("Still waiting...");
- // Wait maximum of 1 second
- // for MessageLoop thread
- // to finish.
- t.join(1000);
- if (((System.currentTimeMillis() - startTime) > patience)
- && t.isAlive()) {
- threadMessage("Tired of waiting!");
- t.interrupt();
- // Shouldn't be long now
- // -- wait indefinitely
- t.join();
- }
- }
- threadMessage("Finally!");
- }
- }
首先來看一張圖,下面這張圖很清晰的說明了執行緒的狀態與Thread中的各個方法之間的關係,很經典的!
要注意的是Thread類也實現了Runnable介面,因此,從Thread類繼承的類的例項也可以作為target傳入這個構造方法。可通過這種方法實現多個執行緒的資源共享。
執行緒的生命週期:
1.新建狀態(New):用new語句建立的執行緒物件處於新建狀態,此時它和其它的java物件一樣,僅僅在堆中被分配了記憶體2.就緒狀態(Runnable):當一個執行緒建立了以後,其他的執行緒呼叫了它的start()方法,該執行緒就進入了就緒狀態。處於這個狀態的 執行緒位於可執行池中,等待獲得CPU的使用權
3.執行狀態(Running): 處於這個狀態的執行緒佔用CPU,執行程式的程式碼
4.阻塞狀態(Blocked): 當執行緒處於阻塞狀態時,java虛擬機器不會給執行緒分配CPU,直到執行緒重新進入就緒狀態,它才有機會轉到 執行狀態。
阻塞狀態分為三種情況:
1)、 位於物件等待池中的阻塞狀態:當執行緒執行時,如果執行了某個物件的wait()方法,java虛擬機器就回把執行緒放到這個物件的等待池中
2)、 位於物件鎖中的阻塞狀態,當執行緒處於執行狀態時,試圖獲得某個物件的同步鎖時,如果該物件的同步鎖已經被其他的執行緒佔用,JVM就會把這個執行緒放到這個物件的瑣池中。
3)、 其它的阻塞狀態:當前執行緒執行了sleep()方法,或者呼叫了其它執行緒的join()方法,或者發出了I/O請求時,就會進入這個狀態中。
一、建立並執行執行緒
當呼叫start方法後,執行緒開始執行run方法中的程式碼。執行緒進入執行狀態。可以通過Thread類的isAlive方法來判斷執行緒是否處於執行狀態。當執行緒處於執行狀態時,isAlive返回true,當isAlive返回false時,可能執行緒處於等待狀態,也可能處於停止狀態。
二、掛起和喚醒執行緒
一但執行緒開始執行run方法,就會一直到這個run方法執行完成這個執行緒才退出。但線上程執行的過程中,可以通過兩個方法使執行緒暫時停止執行。這兩個方法是suspend和sleep。在使用suspend掛起執行緒後,可以通過resume方法喚醒執行緒。而使用sleep使執行緒休眠後,只能在設定的時間後使執行緒處於就緒狀態(線上程休眠結束後,執行緒不一定會馬上執行,只是進入了就緒狀態,等待著系統進行排程)。suspend方法是不釋放鎖雖然suspend和resume可以很方便地使執行緒掛起和喚醒,但由於使用這兩個方法可能會造成一些不可預料的事情發生,因此,這兩個方法被標識為deprecated(棄用)標記,這表明在以後的jdk版本中這兩個方法可能被刪除,所以儘量不要使用這兩個方法來操作執行緒。
三、終止執行緒的三種方法
有三種方法可以使終止執行緒。1. 使用退出標誌,使執行緒正常退出,也就是當run方法完成後執行緒終止。
2. 使用stop方法強行終止執行緒(執行緒中呼叫了阻塞程式碼)(這個方法不推薦使用,因為stop是依靠丟擲異常來結束執行緒的,也可能發生不可預料的結果)。如果沒有呼叫阻塞程式碼,可以正常結束執行緒。
3. 使用interrupt方法中斷執行緒(執行緒中呼叫了阻塞程式碼)(其實這種方法也是通過丟擲異常來結束執行緒的)。如果沒有呼叫阻塞程式碼,可以通過判斷執行緒的中斷標誌位來介紹執行緒。
執行緒的幾個方法:
join():等待此執行緒死亡後再繼續,可使非同步執行緒變為同步執行緒,join方法是不會釋放鎖interrupt():中斷執行緒,被中斷執行緒會拋InterruptedException
wait():等待獲取鎖:表示等待獲取某個鎖執行了該方法的執行緒釋放物件的鎖,JVM會把該執行緒放到物件的等待池中。該執行緒等待其它執行緒喚醒
notify():執行該方法的執行緒喚醒在物件的等待池中等待的一個執行緒,JVM從物件的等待池中隨機選擇一個執行緒,把它轉到物件的鎖池中。使執行緒由阻塞佇列進入就緒狀態
sleep():讓當前正在執行的執行緒休眠,有一個用法可以代替yield函式——sleep(0)
yield():暫停當前正在執行的執行緒物件,並執行其他執行緒。也就是交出CPU一段時間(其他同樣的優先順序或者更高優先順序的執行緒可以獲取到執行的機會)
sleep和yield區別:
1、sleep()方法會給其他執行緒執行的機會,而不考慮其他執行緒的優先順序,因此會給較低執行緒一個執行的機會;yield()方法只會給相同優先順序或者更高優先順序的執行緒一個執行的機會。
2、當執行緒執行了sleep(long millis)方法後,將轉到阻塞狀態,引數millis指定睡眠時間;當執行緒執行了yield()方法後,將轉到就緒狀態。
3、sleep()方法宣告丟擲InterruptedException異常,而yield()方法沒有宣告丟擲任何異常
4、sleep()方法比yield()方法具有更好的移植性
如果希望明確地讓一個執行緒給另外一個執行緒執行的機會,可以採取以下的辦法之一:1、調整各個執行緒的優先順序
2、讓處於執行狀態的執行緒呼叫Thread.sleep()方法
3、讓處於執行狀態的執行緒呼叫Thread.yield()方法
4、讓處於執行狀態的執行緒呼叫另一個執行緒的join()方法
首先,wait()和notify(),notifyAll()是Object類的方法,sleep()和yield()是Thread類的方法。(1).常用的wait方法有wait()和wait(long timeout):
void wait() 在其他執行緒呼叫此物件的 notify() 方法或 notifyAll() 方法前,導致當前執行緒等待。
void wait(long timeout) 在其他執行緒呼叫此物件的 notify() 方法或 notifyAll() 方法,或者超過指定的時間量前,導致當前執行緒等待。
wait()後,執行緒會釋放掉它所佔有的“鎖標誌”,從而使執行緒所在物件中的其它synchronized資料可被別的執行緒使用。
wait()和notify()因為會對物件的“鎖標誌”進行操作,所以它們必須在synchronized函式或synchronized程式碼塊中進行呼叫。如果在non- synchronized函式或non-synchronized程式碼塊中進行呼叫,雖然能編譯通過,但在運 行時會發生IllegalMonitorStateException的異常。
(2).Thread.sleep(long millis),必須帶有一個時間引數。sleep(long)使當前執行緒進入停滯狀態,所以執行sleep()的執行緒在指定的時間內肯定不會被執行;
sleep(long)可使優先順序低的執行緒得到執行的機會,當然也可以讓同優先順序和高優先順序的執行緒有執行的機會;
sleep(long)是不會釋放鎖標誌的。
(3).yield()沒有引數。sleep 方法使當前執行中的執行緒睡眼一段時間,進入不可執行狀態,這段時間的長短是由程式設定的,yield 方法使當前執行緒讓出CPU佔有權,但讓出的時間是不可設定的。yield()也不會釋放鎖標誌。
實際上,yield()方法對應瞭如下操作: 先檢測當前是否有相同優先順序的執行緒處於同可執行狀態,如有,則把 CPU 的佔有權交給此執行緒,否則繼續執行原來的執行緒。所以yield()方法稱為“退讓”,它把執行機會讓給了同等優先順序的其他執行緒。
sleep方法允許較低優先順序的執行緒獲得執行機會,但yield()方法執行時,當前執行緒仍處在可執行狀態,所以不可能讓出較低優先順序的執行緒些時獲得CPU佔有權。 在一個執行系統中,如果較高優先順序的執行緒沒有呼叫 sleep 方法,又沒有受到 I/O阻塞,那麼較低優先順序執行緒只能等待所有較高優先順序的執行緒執行結束,才有機會執行。
yield()只是使當前執行緒重新回到可執行狀態,所以執行yield()的執行緒有可能在進入到可執行狀態後馬上又被執行。所以yield()只能使同優先順序的執行緒有執行的機會。
volitile 語義
volatile相當於synchronized的弱實現,也就是說volatile實現了類似synchronized的語義,卻又沒有鎖機制。它確保對volatile欄位的更新以可預見的方式告知其他的執行緒。
volatile包含以下語義:
(1)Java 儲存模型不會對valatile指令的操作進行重排序:這個保證對volatile變數的操作時按照指令的出現順序執行的。
(2)volatile變數不會被快取在暫存器中(只有擁有執行緒可見)或者其他對CPU不可見的地方,每次總是從主存中讀取volatile變數的結果。也就是說對於volatile變數的修改,其它執行緒總是可見的,並且不是使用自己執行緒棧內部的變數。也就是在happens-before法則中,對一個valatile變數的寫操作後,其後的任何讀操作理解可見此寫操作的結果。
儘管volatile變數的特性不錯,但是volatile並不能保證執行緒安全的,也就是說volatile欄位的操作不是原子性的,volatile變數只能保證可見性(一個執行緒修改後其它執行緒能夠理解看到此變化後的結果),要想保證原子性,目前為止只能加鎖!
資料同步:
執行緒同步的特徵:1、如果一個同步程式碼塊和非同步程式碼塊同時操作共享資源,仍然會造成對共享資源的競爭。因為當一個執行緒執行一個物件的同步程式碼塊時,其他的執行緒仍然可以執行物件的非同步程式碼塊。(所謂的執行緒之間保持同步,是指不同的執行緒在執行同一個物件的同步程式碼塊時,因為要獲得物件的同步鎖而互相牽制)
2、每個物件都有唯一的同步鎖
3、在靜態方法前面可以使用synchronized修飾符,但是要注意的是鎖物件是類(用Object.class而不能用this),而不是這個類的物件。
4、當一個執行緒開始執行同步程式碼塊時,並不意味著必須以不間斷的方式執行,進入同步程式碼塊的執行緒可以執行Thread.sleep()或者執行Thread.yield()方法,此時它並不釋放物件鎖,只是把執行的機會讓給其他的執行緒。
5、synchronized宣告不會被繼承,如果一個用synchronized修飾的方法被子類覆蓋,那麼子類中這個方法不在保持同步,除非用synchronized修飾。
6、synchronized 關鍵字能夠修飾一個物件例項中的函式或者程式碼塊。 在一個非靜態方法中 this 關鍵字表示當前的例項物件。 在一個 synchronized 修飾的靜態的方法中,這個方法所在的類使用 Class 作為例項物件