@Java | Thread & synchronized – [ 多執行緒 基本使用]

NO IMAGE

本文及後續相關文章梳理一下關於多執行緒和同步鎖的知識,平時只是應用層面的瞭解,由於最近面試總是問一些原理性的知識,雖說比較反感這種理論派,但是為了生計也必須掌握一番。(PS:並不是說掌握原理不好,但是封裝就是為了更好的應用,個人感覺沒必要為了學習而學習,比較傾向於行動派,能將原理應用到實際才算參透,本文也僅僅是背書而已)

知識點

  • 程序:程序就是一段程式的執行過程,指在系統中能獨立執行並作為資源分配的基本單位,它是由一組機器指令、資料和堆疊等組成的,是一個能獨立執行的活動實體。
  • 執行緒:執行緒是程序中的一個實體,作為系統排程和分派的基本單位。Linux下的執行緒看作輕量級程序。

執行緒和程序一樣分為五個階段:建立、就緒、執行、阻塞、終止
多程序是指作業系統能同時執行多個任務(程式)
多執行緒是指在同一程式中有多個順序流在執行

多執行緒使用說明

如何建立執行緒

  • 實現Runnable介面
  • 繼承Thread類
  • 通過Callable和Future建立執行緒

1. 通過實現Runnable介面建立並執行執行緒

– 實現Runnable介面

Public class A implements Runnable {
public void run () {    // 必須實現run方法
// 執行緒執行的業務程式碼
}
}

上面實現了Runnable介面並通過run方法包裝了需要通過執行緒執行的程式碼

– 執行執行緒
通過實現了Runnable介面的類的例項建立執行緒物件(Thread)來執行執行緒,常用的Thread構造方法:

Thread(Runnable threadOb,String threadName);

其中threadOb 是一個實現Runnable介面的類的例項threadName指定執行緒的名字

呼叫執行緒類中的start()方法執行執行緒 new Thread(threadOb,threadName).start();(可建立多個執行緒物件執行同一個Runnable介面例項的run方法,實現資源共享

PS:start()方法的呼叫後並不是立即執行多執行緒程式碼,而是使得該執行緒變為可執行態(Runnable),等待CPU分配排程執行

2. 繼承Thread類建立並執行執行緒

– 繼承Thread類

public class A extends Thread {
@Override
public void run() {    // 重寫run方法
// 執行緒執行的業務程式碼
}
}

上面繼承了Runnable介面並通過重寫run方法包裝了需要通過執行緒執行的程式碼,通過原始碼可以看到Thread類也是實現了Runnable介面的,所以本質上和上一種方式無太大區別,不同的是Thread類不適合共享資源執行緒實現

– 執行執行緒

同樣是呼叫執行緒類中的start()方法執行執行緒,此時執行緒類為繼承Thread的類

3. 通過Callable和Future建立並執行執行緒

– 實現Callable介面

public class A implements Callable<T> {
@Override  
public T call() throws Exception  // 實現call()方法
{  
//  執行緒執行的業務程式碼
}  
}

建立Callable介面的實現類(通過泛型制定執行緒執行結束後的返回值型別),並實現call()方法,該call()方法將作為執行緒執行體,並且有返回值(返回值型別為Callable介面泛型制定的型別)

– 使用FutureTask類來包裝Callable物件

FutureTask<T> ft = new FutureTask<>(callableObj);

其中callableObjCallable實現類的例項,使用FutureTask類來包裝Callable物件,該FutureTask物件封裝了該Callable物件call()方法的返回值

– 執行執行緒

通過FutureTask類的例項建立執行緒物件(Thread)來執行執行緒,此時應用的Thread`構造方法:

Thread(FutureTask futureObj,String threadName);

其中futureObj 是一個FutureTask 類的例項threadName指定執行緒的名字

呼叫執行緒類中的start()方法執行執行緒 new Thread(threadOb,threadName).start();
呼叫FutureTask物件的get()方法來獲得子執行緒執行結束後的返回值

使用須知

  1. 多執行緒執行的時候是無序的,即誰先獲取到CPU資源就可以先執行,隨機性比較大
  2. 如果start()方法重複呼叫,會出現java.lang.IllegalThreadStateException異常
  3. 直接繼承Thread類和實現介面方式建立執行緒的區別

    • 直接繼承Thread類方便快捷,適合相對單一無序的多執行緒執行
    • 實現介面方式適合多個相同的程式程式碼的執行緒去處理同一個資源
    • 實現介面方式可以避免java中的單繼承的限制
    • 實現介面方式增加程式的健壯性,程式碼可以被多個執行緒共享,程式碼和資料獨立
    • 執行緒池只能放入實現Runablecallable類執行緒,不能直接放入繼承Thread的類
  4. Java程式執行首先會啟動一個JVM程序,然後至少啟動兩個執行緒,即main執行緒垃圾收集執行緒

Thread常用方法說明

  1. sleep(long millis): 在指定的毫秒數內讓當前正在執行的執行緒休眠(暫停執行),可用TimeUnit.MILLISECONDS.sleep方法替換
  2. join(): 等待呼叫join()的執行緒終止

    • 使用方式

      `Thread t = new AThread(); t.start(); t.join();`

      join()的作用是將執行緒加入到當前執行緒中,只有執行完join()呼叫執行緒才能執行後面的程式碼

    • 使用場景
      正常情況下主執行緒不依賴子執行緒執行完成而結束,當主執行緒需要在子執行緒完成之後再結束時,可使用此方法
  3. yield(): 暫停當前正在執行的執行緒物件,並執行其他執行緒

    • 使用說明
      yield()只是將執行緒從執行狀態轉到可執行狀態(start()方法執行後的狀態),不會導致執行緒轉到等待/睡眠/阻塞狀態
    • 使用場景
      yield()應該做的是讓當前執行執行緒回到可執行狀態,以允許具有相同優先順序的其他執行緒獲得執行機會。因此,使用yield()的目的是讓相同優先順序的執行緒之間能適當的輪轉執行。但是,實際中無法保證yield()達到讓步目的,因為讓步的執行緒還有可能被執行緒排程程式再次選中

      PS:yield()方法執行時,當前執行緒仍處在可執行狀態,所以,不可能讓出較低優先順序的執行緒些時獲得CPU佔有權。在一個執行系統中,如果較高優先順序的執行緒沒有呼叫sleep方法,又沒有受到I\O 阻塞,那麼,較低優先順序執行緒只能等待所有較高優先順序的執行緒執行結束,才有機會執行

  4. setPriority(): 更改執行緒的優先順序,優先順序高的執行緒會獲得較多的執行機會
    優先順序靜態常量MIN_PRIORITY=1,NORM_PRIORITY=5,MAX_PRIORITY=10

    • 使用方式

      Thread t1 = new Thread("t1");
      Thread t2 = new Thread("t2");
      t1.setPriority(Thread.MAX_PRIORITY);
      t2.setPriority(Thread.MIN_PRIORITY);
    • 使用說明
      Thread類的setPriority()和getPriority()方法分別用來設定和獲取執行緒的優先順序。
      每個執行緒都有預設的優先順序。主執行緒的預設優先順序為Thread.NORM_PRIORITY。
      執行緒的優先順序有繼承關係,比如A執行緒中建立了B執行緒,那麼B將和A具有相同的優先順序。
      JVM提供了10個執行緒優先順序,但與常見的作業系統都不能很好的對映。如果希望程式能移植到各個作業系統中,應該僅僅使用Thread類有以下三個靜態常量作為優先順序,這樣能保證同樣的優先順序採用了同樣的排程方式
  5. interrupt(): 將執行緒物件的中斷標識設成true

    • 使用說明

      • 中斷只是一種協作機制,Java沒有給中斷增加任何語法,中斷的過程完全需要程式設計師自己實現。若要中斷一個執行緒,你需要手動呼叫該執行緒的interrupted方法,該方法也僅僅是將執行緒物件的中斷標識設成true;接著你需要自己寫程式碼不斷地檢測當前執行緒的標識位;如果為true,表示別的執行緒要求這條執行緒中斷,此時究竟該做什麼需要你自己寫程式碼實現
      • 每個執行緒物件中都有一個標識,用於表示執行緒是否被中斷;該標識位為true表示中斷,為false表示未中斷
      • 通過呼叫執行緒物件的interrupt方法將該執行緒的標識位設為true;可以在別的執行緒中呼叫,也可以在自己的執行緒中呼叫
    • 使用方式

      • 在呼叫阻塞方法時正確處理InterruptedException異常
      • 設定中斷監聽(另一種方式)

        Thread t1 = new Thread( new Runnable(){
        public void run(){
        // 若未發生中斷,就正常執行任務
        while(!Thread.currentThread.isInterrupted()){
        // 正常任務程式碼……
        }
        // 中斷的處理程式碼……
        doSomething();
        }
        } ).start();
      • 觸發中斷

        t1.interrupt();
    • interrupt方法使用參考連結
      大閒人柴毛毛
  6. wait(): 主動釋放物件鎖,同時本執行緒休眠,直到有其它執行緒呼叫物件的notify()喚醒該執行緒,重新獲取物件鎖並執行(wait()方法屬於Object中的方法,並不屬於Thread類)
  7. notify(): 喚醒呼叫notify()物件的執行緒,notify()呼叫後,並不是馬上就釋放物件鎖的,而是在相應的synchronized(){}語句塊執行結束,自動釋放鎖後,JVM會在wait()物件鎖的執行緒中隨機選取一執行緒,賦予其物件鎖,喚醒執行緒,繼續執行

Thread幾個方法比較

1.sleep()yield()的區別

  • sleep()使當前執行緒進入停滯狀態,所以執行sleep()的執行緒在指定的時間內肯定不會被執行;yield()只是使當前執行緒重新回到可執行狀態,所以執行yield()的執行緒有可能在進入到可執行狀態後馬上又被執行
  • sleep方法使當前執行中的執行緒睡眼一段時間,進入不可執行狀態,這段時間的長短是由程式設定的,yield方法使當前執行緒讓出CPU佔有權,但讓出的時間是不可設定的。實際上,yield()方法對應瞭如下操作:先檢測當前是否有相同優先順序的執行緒處於同可執行狀態,如有,則把CPU的佔有權交給此執行緒,否則,繼續執行原來的執行緒。所以yield()方法稱為’退讓’,它把執行機會讓給了同等優先順序的其他執行緒
  • sleep方法允許較低優先順序的執行緒獲得執行機會,但yield()方法執行時,當前執行緒仍處在可執行狀態,所以,不可能讓出較低優先順序的執行緒些時獲得CPU佔有權。在一個執行系統中,如果較高優先順序的執行緒沒有呼叫 sleep方法,又沒有受到IO阻塞,那麼,較低優先順序執行緒只能等待所有較高優先順序的執行緒執行結束,才有機會執行

2.wait()sleep()區別

共同點:

  • 他們都是在多執行緒的環境下,都可以在程式的呼叫處阻塞指定的毫秒數,並返回
  • wait()和sleep()都可以通過interrupt()方法 打斷執行緒的暫停狀態,從而使執行緒立刻丟擲InterruptedException
  • 如果執行緒A希望立即結束執行緒B,則可以對執行緒B對應的Thread例項呼叫interrupt方法。如果此刻執行緒B正在wait/sleep /join,則執行緒B會立刻丟擲InterruptedException,在catch() {} 中直接return即可安全地結束執行緒。

需要注意的是,InterruptedException是執行緒自己從內部丟擲的,並不是interrupt()方法丟擲的。對某一執行緒呼叫interrupt()時,如果該執行緒正在執行普通的程式碼,那麼該執行緒根本就不會丟擲InterruptedException。但是,一旦該執行緒進入到wait()/sleep()/join()後,就會立刻丟擲InterruptedException

不同點:

  • sleep(),yield()等是Thread類的方法
  • wait()和notify()等是Object的方法
  • 每個物件都有一個鎖來控制同步訪問。Synchronized關鍵字可以和物件的鎖互動,來實現執行緒的同步。sleep方法沒有釋放鎖,而wait方法釋放了鎖,使得其他執行緒可以使用同步控制塊或者方法
  • wait,notify和notifyAll只能在同步控制方法或者同步控制塊裡面使用,而sleep可以在任何地方使用

簡單來說: 

  • sleep()睡眠時,保持物件鎖,仍然佔有該鎖;
  • 而wait()睡眠時,釋放物件鎖。
  • wait()和sleep()都可以通過interrupt()方法打斷執行緒的暫停狀態,從而使執行緒立刻丟擲InterruptedException(但不建議使用該方法)

sleep()方法

  • sleep()使當前執行緒進入停滯狀態(阻塞當前執行緒),讓出CPU的使用、目的是不讓當前執行緒獨自霸佔該程序所獲的CPU資源,以留一定時間給其他執行緒執行的機會;
  • sleep()是Thread類的Static的方法;因此他不能改變物件的機鎖,所以當在一個Synchronized塊中呼叫Sleep()方法是,執行緒雖然休眠了,但是物件的機鎖並沒有被釋放,其他執行緒無法訪問這個物件(即使休眠也持有物件鎖
  • 在sleep()休眠時間期滿後,該執行緒不一定會立即執行,這是因為其它執行緒可能正在執行而且沒有被排程為放棄執行,除非此執行緒具有更高的優先順序

wait()方法

  • wait()方法是Object類裡的方法;當一個執行緒執行到wait()方法時,它就進入到一個和該物件相關的等待池中,同時失去(釋放)了物件的機鎖(暫時失去機鎖,wait(long timeout)超時時間到後還需要返還物件鎖);其他執行緒可以訪問;
  • wait()使用notify或者notifyAlll或者指定睡眠時間來喚醒當前等待池中的執行緒。
  • wait()必須放在synchronized block中,否則會在程式runtime時扔出java.lang.IllegalMonitorStateException異常。