Java—多執行緒基礎

鹿小洋的Java筆記發表於2017-12-12

基本概念

程式

所謂程式就是執行在作業系統的一個任務,程式是計算機任務排程的一個單位,作業系統在啟動一個程式的時候,會為其建立一個程式,JVM就是一個程式。程式與程式之間是相互隔離的,每個程式都有獨立的記憶體空間。

計算機實現併發的原理是:CPU分時間片,交替執行,巨集觀並行,微觀序列。同理,在程式的基礎上分出更小的任務排程單元就是執行緒,我們所謂的多執行緒就是一個程式併發多個執行緒。

執行緒

在上面我們提到,一個程式可以併發出多個執行緒,而執行緒就是最小的任務執行單元,具體來說,一個程式順序執行的流程就是一個執行緒,我們常見的main就是一個執行緒(主執行緒)。

執行緒的組成

想要擁有一個執行緒,有這樣的一些不可或缺的部分,主要有:CPU時間片,資料儲存空間,程式碼。

CPU時間片都是有作業系統進行分配的,資料儲存空間就是我們常說的堆空間和棧空間,線上程之間,堆空間是多執行緒共享的,棧空間是互相獨立的,這樣做的好處不僅在於方便,也減少了很多資源的浪費。程式碼就不做過多解釋了,沒有程式碼搞個毛的多執行緒。

執行緒的建立和啟動
傳統建立執行緒有兩種方式
  1. 繼承Thread類,覆蓋run方法

  2. 實現Runnable介面,覆蓋run方法

Runnable並不是執行緒物件,而是一個任務物件。那麼Runnable和Thread有什麼樣的關係呢?

通過查閱API,我們發現建立一個執行緒除了使用Thread的無參構造方法以外有一個有參構造方法是這樣 :Thread(Runnable target),通過這個方法會分配一個新的Thread 物件。

其中的引數是一個型別為Runnable的target屬性。

Runnable介面最大的作用就是為非Thread子類的類提供了一種實現執行緒的方式,只需要實現Runnable介面就可以藉助Thread建立一個執行緒;另一方面,如果只想重寫run方法,不想得到其他的Thread的方法,實現Runnable是一個好的選擇。

JDK1.5

執行緒池

ExecutorService(執行緒池 interface)

//通過工具類中的方法能夠新建一個執行緒池,用ExecutorService接受
ExecutorService es = Executors.newFixedThreadPool(2);

複製程式碼

Callable物件

類似於Runnable(描述任務的interface)。

//建立一個Callable的實現類
Callable<Integer> task1 = new Callable<Integer>(){
  public Integer call() throws Exception{
    int result = 0;
    for(int i=2;i<=100;i+=2){
      result += i;
      Thread.sleep;
    }
      return result;
  }
  
}

//用Future物件接收fask1的返回值  將任務提交給執行緒池
Future<Integer> f = es.submit(task1);
//通過get方法獲取Future中的值 在這個時候主執行緒主動的調取get  如果分支執行緒還沒有結束,主執行緒會在這裡阻塞
int result = f1.get();
//關閉執行緒池
es.shutdown();
複製程式碼

從以上這段程式碼我們可以看到很多不一樣的地方,首先在Callable物件中是可以丟擲異常的,其次有返回值,在這個基礎上也就引出了一個新的問題,如果接收該執行緒的物件?JDK1.5中也給出瞭解決的方法是Future物件.

啟動執行緒

在這裡我們需要明白,上面兩種方式並不會讓我們得到真正的執行緒,只是得到了執行緒物件,只有啟動執行緒,才算得到了真正的執行緒。

通過執行start()方法能夠啟動一個執行緒,但是啟動執行緒並不是立即執行,成功啟動的執行緒會處於就緒狀態,什麼時候執行需要等到拿到時間片之後。

執行緒的分類

使用者執行緒和守護(Daemon)執行緒。

守護執行緒:守護執行緒會一直執行,直到其他非守護執行緒都結束的時候,才會結束。有一個典型的守護執行緒就是:垃圾回收執行緒,和虛擬機器共存亡,直到虛擬機器中沒有任何執行緒的時候虛擬機器關閉的時候才會終止,簡單說就是虛擬機器在,它就在,虛擬機器亡便亡。

執行緒的狀態

執行緒的狀態

上面我們提到過,一個執行緒在啟動之後不會立馬執行,而是處於就緒狀態(Ready),就緒狀態就是執行緒的狀態的一種,處於這種狀態的執行緒意味著一切準備就緒, 需要等待系統分配到時間片。為什麼沒有立馬執行呢,因為同一時間只有一個執行緒能夠拿到時間片執行,新執行緒啟動的時候讓它啟動的執行緒(主執行緒)正在執行,只有等主執行緒結束,它才有機會拿到時間片執行。

**執行緒的狀態:**初始狀態(New),就緒狀態(Ready),執行狀態(Running)(特別說明:在語法的定義中,就緒狀態和執行狀態是一個狀態Runable),等待狀態(Waitering),終止狀態(Terminated)

RUNNABLE),等待狀態(Waitering),終止狀態(Terminated)

初始狀態(New)

執行緒物件被建立出來,便是初始狀態,這時候執行緒物件只是一個普通的物件,並不是一個執行緒。

Runable

**就緒狀態(Ready):**執行start方法之後,進入就緒狀態,等待被分配到時間片。

**執行狀態(Running):**拿到CPU的執行緒開始執行。處於執行時間的執行緒並不是永久的持有CPU直到執行結束,很可能沒有執行完畢時間片到期,就被收回CPU的使用權了,之後將會處於等待狀態。

等待狀態(Waiting)

等待狀態分為有限期等待和無限期等待,所謂有限期等待是執行緒使用sleep方法主動進入休眠,有一定的時間限制,時間到期就重新進入就緒狀態,再次等待被CPU選中。

而無限期等待就有些不同了,無限期並不是指永遠的等待下去,而是指沒有時間限制,可能等待一秒也可能很多秒。至於進入等待的原因也不盡相同,可能是因為CPU時間片到期,也可能是因為一個比較耗時的操作(資料庫),或者主動的呼叫join方法。

wait和sleep的區別

wait sleep
wait()方法是Object類裡的方法 sleep()是Thread類的static(靜態)的方法
wait()睡眠時,釋放物件鎖 sleep()睡眠時,保持物件鎖,仍然佔有該鎖
常用於執行緒間通訊 常用於暫停執行
wait和notify/notifyAll是成對出現的, 必須在synchronize塊中被呼叫
阻塞狀態(Blocked)

在我看來,阻塞狀態實際上是一種比較特殊的等待狀態,處於其他等待狀態的執行緒是在等著別的執行緒執行結束,等著拿CPU的使用權;而處於阻塞狀態的執行緒等待的不僅僅是CPU的使用權,主要是鎖標記,沒有拿到鎖標記,即便是CPU有空也沒有辦法執行。(關於鎖見下節:執行緒同步)

等待和阻塞的區別

等待 阻塞
已經拿到鎖物件,或者說不存在拿不到執行不了的情況 等待拿到鎖物件
等待被喚醒 等待拿到鎖物件
終止執行緒(Terminated)

已經終止的執行緒會處於該種狀態。

總結

總體上來說,作為一個執行緒挺倒黴的,首先,不會知道自己什麼時候被選中;其次在執行過程中隨時可能被打斷讓出CPU,最後碰到資料庫等耗時的操作也要讓出CPU去等待,並且就算資料準備好了, 仍然需要等著被挑選。

相關文章