1. 程式人生 > >java基礎知識總結--多執行緒

java基礎知識總結--多執行緒

1、擴充套件Java.lang.Thread類

1.1、程序和執行緒的區別:

       程序:每個程序都有自己獨立的程式碼和資料空間(程序上下文),程序間的切換會有較大的開銷,一個程序包含1~n個執行緒。

       執行緒:同一類執行緒共享程式碼和資料空間,每一個執行緒有獨立的執行棧和程式計數器,執行緒切換開銷比較小。

       程序和執行緒一樣都分為五個階段:建立、就緒、執行、阻塞、終止。

       多程序是指作業系統能同時執行多個任務(程式)。

       多執行緒是指在同一個程式中有多個順序流在執行。

       1.2、在Java中要想實現多執行緒,有兩種方法:繼承Thread類、實現Runable介面。

 

 

程式啟動執行main的時候,Java虛擬機器啟動一個程序,主執行緒main在main()呼叫的時候被建立。隨著呼叫Thread1的兩個物件的start方法,另外兩個執行緒也啟動了,這樣整個應用就在多執行緒下執行。

注意:start()方法的呼叫後並不是立即執行多執行緒程式碼,而是使該執行緒變為可執行狀態(Run able),什麼時候執行是由作業系統決定的。

從程式執行的結果來看,多執行緒程式的執行順序是不確定的,因此,只有亂序執行的程式碼才是有必要設計為多執行緒。Thread.sleep()方法呼叫的目的是不讓當前執行緒霸佔該程序所獲取的CPU資源,已留出一定的世家給其他的執行緒執行的機會。實際上所有的多執行緒程式碼執行順序都是不確定的,每次的執行結果都是隨機的。

如果對同一個物件重複呼叫start()方法的話,會出現java.lang.IllegalThreadStateException的異常。

2、實現Java.lang.Runable介面

 

 

Thread2類通過實現Runable介面,使得該類具有了多執行緒的特徵。run()方法是多執行緒程式的一個約定,所有的多執行緒程式碼都在run()方法裡面。Thread()類實際上也是實現了Runable()介面的類。

在啟動多執行緒的時候,需要先通過Thread類的構造方法Thread(Runable target)構造物件,然後呼叫Thread物件的start()方法來執行多執行緒程式碼。

實際上所有的多執行緒程式碼都是通過執行Thread類的start()方法來執行的。因此不管是繼承Thread類還是實現Runable介面來實現多執行緒,最終還是通過Thread的物件的API來控制執行緒的,熟悉Thread類的API是進行多執行緒程式設計的基礎。

3、Thread和Runable 的區別

如果一個類繼承Thread,那麼不適合資源共享。但是如果實現了Runable介面的話,則很容易實現資源共享。

這段程式碼Thread1繼承了Thread類的情況:

 

 

從執行結果來看,不同的執行緒之間count是不同的,這樣的情況對於售票系統來說就會有很大的問題,當然這裡可以用同步來解決。

這段程式碼是Thread2實現了Runable介面的情況:

 

 如果測試類和上邊的建立Thread2的方法不一樣:

 

上面的兩種建立方法主要是為了說明建立完成的Thread2類物件再接著建立Thread類物件的時候,呼叫的構造器不同,輸出的結構也是不一樣的。後面那個引數可以 表示一個執行緒名字如果不指定的話就會生成一個預設的名字。

這裡需要注意的是每個執行緒都是用同一個例項化物件,如果不是同一個,那效果就會上邊用Thread1繼承Thread的一樣了。

       總結:實現Runable介面比繼承Thread類所具有的優勢:

  •  適合同一個程式程式碼的多個執行緒去處理同一個資源。
  •  可以避免Java中的單繼承限制
  •  增加程式的健壯性,程式碼可以被多個執行緒共享,程式碼和資料獨立。

需要特別提醒:main()方法也是一個執行緒,在Java中的所有的執行緒都是同時啟動的,至於是什麼時候開始執行、哪個會先執行完全看哪個執行緒最先得到CPU資源。在Java中每次程式執行至少啟動兩個執行緒,一個是main執行緒,另一個是垃圾收集執行緒。因為每當使用Java命令執行一個類的時候,實際上就會啟動一個Java虛擬機器,每一個Java虛擬機器在就相當於是在作業系統中啟動了一個程序。

4、執行緒狀態的轉換

 

 

4.1、新建狀態:新建立了一個執行緒物件。

4.2、就緒狀態::執行緒物件建立好了以後,改狀態的執行緒位於可執行執行緒狀態池中,變得可以執行,只需要等待獲取到CPU資源。

4.3、執行狀態:就緒狀態的執行緒獲取到CPU資源變成執行狀態指向程式程式碼。

4.4、阻塞狀態:因為某種原因需要放棄CPU使用權,暫停程式的執行,直到執行緒進入就緒狀態才有機會轉入執行狀態。阻塞分為三種情況:

  • 等待阻塞:執行的執行緒執行wait()方法,JVM會把該執行緒放入等待池中
  •  同步阻塞:執行的執行緒在獲取物件的同步鎖時,若該同步鎖被別的執行緒佔用,則JVM會把該執行緒放入鎖池中。
  • 其他阻塞:執行的執行緒執行sleep()或者join()方法,或是發出了I/O請求時,JVM會把該執行緒置為阻塞狀態。但需要注意的是當sleep()狀態超時、join()等待執行緒結束或者超時、I/O處理完畢的時候,執行緒重新進入就緒狀態。

4.5、死亡狀態:執行緒執行完或者因異常退出了run()方法時,該執行緒的生命週期結束。

5、執行緒排程

5.1、調整執行緒的優先順序:Java執行緒有優先順序,優先順序高的執行緒會有獲得較多的執行機會。

Java執行緒的優先順序用整數表示,取值範圍是1~10,Thread類有三個靜態常量:

  •  static int MAX_PRIORITY 執行緒具有最高的優先順序,10
  •  static int MIN_PRIORITY 執行緒具有最低的優先順序 1
  • static int NORM_PRIORITY 分配給執行緒的預設優先順序 5

Thread類的setPriority() 和getPriority()方法分別是用來設定和獲取執行緒優先順序的方法。每個執行緒都有一個預設的優先順序。主執行緒的預設優先順序為5,執行緒的優先順序有繼承關係,比如說A執行緒中建立了B執行緒,那麼B和A具有相同的優先順序。JVM提供了10個執行緒優先順序,但是與常見的作業系統都不能很高的對映,如果希望程式能夠移植到各個作業系統上,應該僅僅使用Thread類有的三個靜態常量作為優先順序,這樣能保證同樣的優先順序採用了同樣的排程方式。

5.2、執行緒睡眠:Thread.sleep(long  millis)方法是執行緒轉到阻塞狀態,millis引數設定睡眠時間,單位為毫秒,當睡眠結束時,就會自動轉為就緒狀態。該靜態方法的移植性比較好。

5.3、執行緒等待:Object類中的wait()方法將會使當前的程序進入等待狀態,直到其他執行緒呼叫此物件的notify()方法或者notifyAll()方法喚醒該執行緒。這兩個喚醒方法也是Object類中的方法,行為等價於呼叫wait(0)方法。

5.4、執行緒讓步:Thread.yield()方法,暫停當前正在執行的執行緒物件,把執行機會讓給相同或者更好優先順序的執行緒。使用該方法的目的是讓相同優先順序的執行緒之間能適當的輪轉執行,但是,實際中無法保證yield()達到讓步的目的,因為讓步的執行緒還是有可能被執行緒排程程式再次選中。

注意:yield()方法從未導致執行緒轉到等待/睡眠/阻塞狀態,在大多數情況下,yield()方法將會導致執行緒從執行狀態轉到可執行狀態,但是有可能沒有效果。

5.5、執行緒加入:join()方法,等待其他執行緒終止,在當前的執行緒中呼叫另一個執行緒的join()方法,則當前執行緒轉入阻塞狀態,直到另一個執行緒執行結束,當前執行緒再有阻塞轉為就緒狀態。

5.6、執行緒喚起:Object類中的notify()方法,喚醒在此物件監視器上等待的單個執行緒,如果所有的執行緒都在此物件等待,則會選擇喚醒一個執行緒,這個選擇是任意的。類似的方法還有一個notifyAll(),喚醒在此物件監視器上等待的所有執行緒。注意:Thread類中的suspend()resume()方法已經被廢除不再使用,因為有死鎖傾向。

6、常用函式的說明

6.1、

Thread.sleep(long  millis)方法:在指定的時間內讓當前正在指向的執行緒休眠。

join()方法:指等待t執行緒終止。join()在啟動執行緒後直接呼叫,他的作用是“等待該執行緒終止”,這裡需要理解的就是該執行緒是指的主執行緒等待子執行緒的終止。也就是在子執行緒呼叫了join()方法以後後面的程式碼只能等到子執行緒結束了以後才能執行。

為什麼要加入join()方法:

很多情況下主執行緒生成並啟動了子執行緒,但是假如子執行緒中有很多需要進行大量耗時的運算,主執行緒往往將於子執行緒之前結束,但是如果主執行緒處理其他的事物需要用到子執行緒的處理結果,也就是主執行緒需要等待子執行緒執行完在結束,這個時候就需要使用join()方法了。

sleep()yield()的區別

sleep()是當前執行緒進入停滯狀態,所以執行完sleep()的執行緒在指定的時間內肯定不會被執行,但是yield()方法只是使當前的執行緒回到可執行狀態,所以執行完yield()方法 的執行緒有可能在進入到可執行狀態後馬上又被執行。

       Sleep()方法使當前執行中的執行緒睡眠一段時間,這段時間是不可執行狀態,這段時間的長短是由程式人為設定的。yield()方法使當前的執行緒讓出CPU佔有權,但是讓出的時間是不可設定的。

       Sleep()方法允許較低優先順序的執行緒獲得執行機會,但是yield()方法執行時,當前執行緒仍然處於可執行狀態,所以不可能讓較低優先順序的執行緒獲得CPU佔有權,在一個執行系統中,如果較高優先順序的執行緒沒有呼叫sleep()方法,有沒有受到阻塞,那麼較低優先順序的執行緒只能等多有較高優先順序的執行緒執行結束才有機會執行。

6.2、interrupt()方法:中斷某一個執行緒,這種結束方式比較粗暴,如果某一個執行緒打開了某一個資源還沒有來得及關閉也就是run()方法還沒有執行完成就強制結束執行緒,會導致資源無法關閉。

6.3、wait()方法:

  Obj.wait()和Object.notify()必須要與synchronized(obj)一起使用,也就是wait和nofity是針對已經獲取了物件鎖進行操作的。從語法的角度來說這兩個方法必須是在synchronized(obj){…}語句塊內,從功能上來說wait就是說執行緒在獲取物件鎖後,主動釋放物件鎖,同時本執行緒休眠。直到有其他的執行緒呼叫物件的nofity()喚醒該執行緒,才能繼續獲取物件鎖並繼續執行。相應的nofity()方法就是對物件鎖的喚醒。但是有一點需要注意nofity()方法呼叫之後並不是馬上就釋放物件鎖而是在相應的synchronized(obj){…}語句塊執行結束。自動釋放以後,JVM會在wait()物件鎖的執行緒中隨機選取一個執行緒,賦予其物件鎖,喚醒執行緒繼續執行。sleep()方法和wait()方法二者都可以暫停當前的執行緒,釋放CPU資源,主要的區別就是wait()在釋放CPU資源的同時釋放了物件鎖的控制。

6.4 wait和sleep的區別:

共同點:

  •  他們都是在多執行緒的環境下都可以在程式的呼叫處阻塞指定的毫秒數,並返回。
  • Wait’和sleep都可以通過interrupt方法打斷執行緒的暫停狀態,從而使執行緒立即丟擲InterruptedException,如果執行緒A希望立即結束執行緒B,則可以對執行緒B對應的Thread實力呼叫interrupt方法。如果執行緒B正在wait/sleep/join/。則執行緒B會立即丟擲InterruptException,在catch中直接return即可安全的結束執行緒。需要注意,這個異常是執行緒自己從內部丟擲來的,並不是interrupt方法丟擲的,對某一個執行緒呼叫interrupt時,如果該執行緒正在執行普通的程式碼,那麼該執行緒不會丟擲異常,除非進入上邊所述的三種狀態其中之一。

不同點:

  • Thread類的方法:sleep(),yield()等 ,   Object的方法:wait()和notify()等 
  •   每個物件都有一個鎖來控制同步訪問。Synchronized關鍵字可以和物件的鎖互動,來實現執行緒的同步。sleep方法沒有釋放鎖,而wait方法釋放了鎖,使得其他執行緒可以使用同步控制塊或者方法。 
  • wait,notify和notifyAll只能在同步控制方法或者同步控制塊裡面使用,而sleep可以在任何地方使用 
  •   sleep必須捕獲異常,而wait,notify和notifyAll不需要捕獲異常,所以sleep()和wait()方法的最大區別是:sleep()睡眠時,保持物件鎖,仍然佔有該鎖;而wait()睡眠時,釋放物件鎖。但是wait()和sleep()都可以通過interrupt()方法打斷執行緒的暫停狀態,從而使執行緒立刻丟擲InterruptedException(但不建議使用該方法)。

7、常見的名詞解釋

  1) 主執行緒:JVM呼叫程式main()方法所產生的執行緒;

  2) 當前執行緒:一般指通過Thread.currentThread()來獲取的程序;

  3) 後臺執行緒:指為其他執行緒提供服務的執行緒,也稱為守護執行緒,JVM的垃圾回收執行緒就是一個後臺執行緒。使用者執行緒和守護執行緒的區別在於是否等待主執行緒,依賴主執行緒結  束而結束。

  4) 前臺執行緒:是指接受後臺服務的執行緒,其實巢狀後臺執行緒是聯絡在一起的,就像傀儡和幕後操作者一樣的關係。

  5) 執行緒類的常用方法:

    1) sleep(): 強迫一個執行緒睡眠N毫秒。 

    2)isAlive(): 判斷一個執行緒是否存活。  

    3) join(): 等待執行緒終止。  

    4)activeCount(): 程式中活躍的執行緒數。 

    5)enumerate(): 列舉程式中的執行緒。

    6)currentThread(): 得到當前執行緒。  

    7)isDaemon(): 一個執行緒是否為守護執行緒。

    8)setDaemon(): 設定一個執行緒為守護執行緒。(使用者執行緒和守護執行緒的區別在於,是否等待主執行緒依賴於主執行緒結束而結束) 

    9) setName(): 為執行緒設定一個名稱。

    10)wait(): 強迫一個執行緒等待。

    11)notify(): 通知一個執行緒繼續執行。

    12)setPriority(): 設定一個執行緒的優先順序。