進階Java多執行緒

LanceToBigData發表於2021-03-10

一、多執行緒建立方式

1.1、繼承Thread類建立執行緒類

1.實現步驟

  • 定義一個繼承Thread類的子類,並重寫該類的run()方法;

  • 建立Thread子類的例項,即建立了執行緒物件;

  • 呼叫該執行緒物件的start()方法啟動執行緒。

2.核心程式碼

class SomeThead extends Thraad   { 
    public void run()   { 
     //do something here  
    }  
 } 
 
public static void main(String[] args){
 SomeThread oneThread = new SomeThread();   
 //啟動執行緒  
 oneThread.start(); 
}

1.2、實現Runnable介面建立執行緒類

1.實現步驟

  • 定義Runnable介面的實現類,並重寫該介面的run()方法;

  • 建立Runnable實現類的例項,並以此例項作為Thread的target物件,即該Thread物件才是真正的執行緒物件。

2.核心程式碼

class SomeRunnable implements Runnable   { 
  public void run()   { 
  //do something here  
  }  
} 
Runnable oneRunnable = new SomeRunnable();   
Thread oneThread = new Thread(oneRunnable);   
oneThread.start();

1.3、通過Callable和Future建立執行緒

1.實現步驟

  • 建立Callable介面的實現類,並實現call()方法,改方法將作為執行緒執行體,且具有返回值。

  • 建立Callable實現類的例項,使用FutrueTask類進行包裝Callable物件,FutureTask物件封裝了Callable物件的call()方法的返回值

  • 使用FutureTask物件作為Thread物件的target建立並啟動新執行緒

  • 呼叫FutureTask物件的get()方法獲取子執行緒執行結束後的返回值。

2.核心程式碼

//1.建立Callable介面的實現類,並實現call()方法
public class SomeCallable01 implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        int i = 0;
        for(;i<10;i++)
        {
            System.out.println(Thread.currentThread().getName()+" "+i);
        }
        return i;
    }

    public static void main(String[] args) {
    	//2.建立Callable實現類的例項
        SomeCallable01 ctt = new SomeCallable01();
        
        //3.使用FutrueTask類進行包裝Callable物件,FutureTask物件封裝了Callable物件的call()方法的返回值
        FutureTask<Integer> ft = new FutureTask<>(ctt);

        //開啟ft執行緒
        for(int i = 0;i < 21;i++)
        {
            System.out.println(Thread.currentThread().getName()+" 的迴圈變數i的值"+i);
            if(i==20)//i為20的時候建立ft執行緒
            {
            	//4.使用FutureTask物件作為Thread物件的target建立並啟動新執行緒
                new Thread(ft,"有返回值的執行緒FutureTask").start();
            }
        }

        //ft執行緒結束時,獲取返回值
        try
        {	
        	//5.呼叫FutureTask物件的get()方法獲取子執行緒執行結束後的返回值。
            System.out.println("子執行緒的返回值:"+ft.get());//get()方法會阻塞,直到子執行緒執行結束才返回
        } catch (InterruptedException e)
        {
            e.printStackTrace();
        } catch (ExecutionException e)
        {
            e.printStackTrace();
        }
    }
}

二、建立執行緒方式的區別

1.使用繼承Thread類的方式建立多執行緒

1)優勢

編寫簡單,如果需要訪問當前執行緒,則無需使用Thread.currentThread()方法,直接使用this即可獲得當前執行緒

2)劣勢

執行緒類已經繼承了Thread類,所以不能再繼承其他父類。(有單繼承的侷限性)

建立多執行緒時,每個任務有成員變數時不共享,必須加static才能做到共享

2.使用實現Runnable類的方式建立多執行緒

1)優勢

避免了單繼承的侷限性、多個執行緒可以共享一個target物件,非常適合多執行緒處理同一份資源的情形。

2)劣勢

比較複雜、訪問執行緒必須使用Thread.currentThread()方法、無返回值。

3.使用實現Callable介面的方式建立多執行緒

1)優勢

有返回值、避免了單繼承的侷限性、多個執行緒可以共享一個target物件,非常適合多執行緒處理同一份資源的情形。

2)劣勢

比較複雜、訪問執行緒必須使用Thread.currentThread()方法

4.Runnable和Callable的區別

1)Callable規定(重寫)的方法是call(),Runnable規定(重寫)的方法是run()。
2)Callable的任務執行後可返回值,而Runnable的任務是不能返回值的。
3)call方法可以丟擲異常,run方法不可以。
4)執行Callable任務可以拿到一個Future物件,表示非同步計算的結果。它提供了檢查計算是否完成的方法,以等待計算的
完成,並檢索計算的結果。通過Future物件可以瞭解任務執行情況,可取消任務的執行,還可獲取執行結果future.get()

三、多執行緒排程

3.1、排程策略

時間片:執行緒的排程採用時間片輪轉的方式
搶佔式:高優先順序的執行緒搶佔CPU

3.2、Java的排程方法

1)對於同優先順序的執行緒組成先進先出佇列(先到先服務),使用時間片策略
2)對高優先順序,使用優先排程的搶佔式策略

3.3、執行緒的優先順序

等級:
MAX_PRIORITY:10
MIN_PRIORITY:1
NORM_PRIORITY:5

方法:

getPriority():返回執行緒優先順序
setPriority(int newPriority):改變執行緒的優先順序

備註:

高優先順序的執行緒要搶佔低優先順序的執行緒的cpu的執行權。但是僅是從概率上來說的,高優先順序的執行緒更有可能被執行。並不意味著只有高優先順序的執行緒執行完以後,低優先順序的執行緒才執行。

四、多執行緒狀態管理

4.1、執行緒睡眠---sleep

1)概述

如果我們需要讓當前正在執行的執行緒暫停一段時間,並進入阻塞狀態,則可以通過呼叫Thread的sleep方法。

2)執行緒睡眠方法

在指定的毫秒數內讓正在執行的執行緒休眠:

sleep(long millis)

在指定的毫秒數加指定的納秒數內讓正在執行的執行緒休眠:

sleep(long millis,int nanos)

3)程式碼實現

sleep是靜態方法,最好不要用Thread的例項物件呼叫它,因為它睡眠的始終是當前正在執行的執行緒,而不是呼叫它的執行緒物件,它只對正在執行狀態的執行緒物件有效。

public class SynTest {
    public static void main(String[] args) {
        new Thread(new CountDown(),"倒數計時").start();
    }
}
 
class CountDown implements Runnable{
    int time = 10;
    public void run() {
        while (true) {
            if(time>=0){
                System.out.println(Thread.currentThread().getName() + ":" + time--);
                try {
                    Thread.sleep(1000);                                                    //睡眠時間為1秒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

4)備註

Java執行緒排程是Java多執行緒的核心,只有良好的排程,才能充分發揮系統的效能,提高程式的執行效率。但是不管程式設計師怎麼編寫排程,只能最大限度的影響執行緒執行的次序,而不能做到精準控制。因為使用sleep方法之後,執行緒是進入阻塞狀態的,只有當睡眠的時間結束,才會重新進入到就緒狀態,而就緒狀態進入到執行狀態,是由系統控制的,我們不可能精準的去幹涉它,所以如果呼叫Thread.sleep(1000)使得執行緒睡眠1秒,可能結果會大於1秒

4.2、執行緒讓步---yield

1)概述

yield()方法和sleep()方法有點相似,它也是Thread類提供的一個靜態的方法,它也可以讓當前正在執行的執行緒暫停,讓出cpu資源給其他的執行緒。但是和sleep()方法不同的是,它不會進入到阻塞狀態,而是進入到就緒狀態。yield()方法只是讓當前執行緒暫停一下,重新進入就緒的執行緒池中,讓系統的執行緒排程器重新排程器重新排程一次,完全可能出現這樣的情況:當某個執行緒呼叫yield()方法之後,執行緒排程器又將其排程出來重新進入到執行狀態執行。

實際上,當某個執行緒呼叫了yield()方法暫停之後,優先順序與當前執行緒相同,或者優先順序比當前執行緒更高的就緒狀態的執行緒更有可能獲得執行的機會,當然,只是有可能,因為我們不可能精確的干涉cpu排程執行緒。

2)程式碼實現

public class Test1 {  
    public static void main(String[] args) throws InterruptedException {  
        new MyThread("低階", 1).start();  
        new MyThread("中級", 5).start();  
        new MyThread("高階", 10).start();  
    }  
}  
  
class MyThread extends Thread {  
    public MyThread(String name, int pro) {  
        super(name);// 設定執行緒的名稱  
        this.setPriority(pro);// 設定優先順序  
    }  
  
    @Override  
    public void run() {  
        for (int i = 0; i < 30; i++) {  
            System.out.println(this.getName() + "執行緒第" + i + "次執行!");  
            if (i % 5 == 0)  
                Thread.yield();  
        }  
    }  
}

3)sleep和yield的區別

① sleep方法暫停當前執行緒後,會進入阻塞狀態,只有當睡眠時間到了,才會轉入就緒狀態。而yield方法呼叫後 ,是直接進入就緒狀態,所以有可能剛進入就緒狀態,又被排程到執行狀態

② sleep方法宣告丟擲了InterruptedException,所以呼叫sleep方法的時候要捕獲該異常,或者顯示宣告丟擲該異常。而yield方法則沒有宣告丟擲任務異常。

③ sleep方法比yield方法有更好的可移植性,通常不要依靠yield方法來控制併發執行緒的執行。

4.3、執行緒合併---join

1)概述

執行緒的合併的含義就是將幾個並行執行緒的執行緒合併為一個單執行緒執行,應用場景是當一個執行緒必須等待另一個執行緒執行完畢才能執行時,Thread類提供了join方法來完成這個功能,注意,它不是靜態方法。

簡而言之:

​ 當B執行緒執行到了A執行緒的.join()方法時,B執行緒就會等待,等A執行緒都執行完畢,B執行緒才會執行。join可以用來臨時加入執行緒執行。

2)執行緒合併方法

它有三個過載方法:

​ 當前執行緒等該加入該執行緒後面,等待該執行緒終止。

void join()

​ 當前執行緒等待該執行緒終止的時間最長為 millis 毫秒。

​ 如果在millis時間內,該執行緒沒有執行完,那麼當前執行緒進入就緒狀態,重新等待cpu排程

void join(long millis)

​ 等待該執行緒終止的時間最長為 millis 毫秒 + nanos

​ 納秒。如果在millis時間內,該執行緒沒有執行完,那麼當前執行緒進入就緒狀態,重新等待cpu排程

void join(long millis,int nanos)

3)程式碼實現

public static void main(String[] args) throws InterruptedException {    
        yieldDemo ms = new yieldDemo();
        Thread t1 = new Thread(ms,"張三吃完還剩");
        Thread t2 = new Thread(ms,"李四吃完還剩");
        Thread t3 = new Thread(ms,"王五吃完還剩");
        t1.start();
        t1.join();
        
        t2.start();
        t3.start();
        System.out.println( "主執行緒");
    }
Thread t = new Thread(() -> {
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    r = 10;
});

t.start();
// 讓主執行緒阻塞 等待t執行緒執行完才繼續執行 
// 去除該行,執行結果為0,加上該行 執行結果為10
t.join();
log.info("r:{}", r);

// 執行結果
13:09:13.892 [main] INFO thread.TestJoin - r:10

4.4、設定執行緒的優先順序

1)概述

每個執行緒執行時都有一個優先順序的屬性,優先順序高的執行緒可以獲得較多的執行機會,而優先順序低的執行緒則獲得較少的執行機會。與執行緒休眠類似,執行緒的優先順序仍然無法保障執行緒的執行次序。只不過,優先順序高的執行緒獲取CPU資源的概率較大,優先順序低的也並非沒機會執行

每個執行緒預設的優先順序都與建立它的父執行緒具有相同的優先順序,在預設情況下,main執行緒具有普通優先順序。

2)涉及優先順序方法

Thread類提供了setPriority(int newPriority)和getPriority()方法來設定和返回一個指定執行緒的優先順序,其中setPriority方法的引數是一個整數,範圍是1~·0之間,也可以使用Thread類提供的三個靜態常量:

MAX_PRIORITY   =10
MIN_PRIORITY   =1
NORM_PRIORITY   =5

3)程式碼實現

public class Test1 {  
        public static void main(String[] args) throws InterruptedException {  
            new MyThread("高階", 10).start();  
            new MyThread("低階", 1).start();  
        }  
    }  
      
    class MyThread extends Thread {  
        public MyThread(String name,int pro) {  
            super(name);//設定執行緒的名稱  
            setPriority(pro);//設定執行緒的優先順序  
        }  
        @Override  
        public void run() {  
            for (int i = 0; i < 100; i++) {  
                System.out.println(this.getName() + "執行緒第" + i + "次執行!");  
            }  
        }  
    }

4)備註

雖然Java提供了10個優先順序別,但這些優先順序別需要作業系統的支援。不同的作業系統的優先順序並不相同,而且也不能很好的和Java的10個優先順序別對應。所以我們應該使用MAX_PRIORITY、MIN_PRIORITY和NORM_PRIORITY三個靜態常量來設定優先順序,這樣才能保證程式最好的可移植性。

4.5、後臺(守護)執行緒

1)概述

守護執行緒使用的情況較少,但並非無用,舉例來說,JVM的垃圾回收、記憶體管理等執行緒都是守護執行緒。還有就是在做資料庫應用時候,使用的資料庫連線池,連線池本身也包含著很多後臺執行緒,監控連線個數、超時時間、狀態等等。

預設情況下,java程式需要等待所有執行緒都執行結束,才會結束,有一種特殊執行緒叫守護執行緒,當所有的非守護執行緒都結束後,即使它沒有執行完,也會強制結束。

2)涉及方法

呼叫執行緒物件的方法setDaemon(true),則可以將其設定為守護執行緒。

將該執行緒標記為守護執行緒或使用者執行緒。當正在執行的執行緒都是守護執行緒時,Java 虛擬機器退出
該方法必須在啟動執行緒前呼叫。 該方法首先呼叫該執行緒的 checkAccess 方法,且不帶任何引數。這可能丟擲 SecurityException(在當前執行緒中)。

public final void setDaemon(boolean on)          
  引數:
     on - 如果為 true,則將該執行緒標記為守護執行緒。    
  丟擲:    
    IllegalThreadStateException - 如果該執行緒處於活動狀態。    
    SecurityException - 如果當前執行緒無法修改該執行緒。

3)守護執行緒的用途

守護執行緒通常用於執行一些後臺作業,例如在你的應用程式執行時播放背景音樂,在文字編輯器裡做自動語法檢查、自動儲存等功能。

java的垃圾回收也是一個守護執行緒。守護線的好處就是你不需要關心它的結束問題。例如你在你的應用程式執行的時候希望播放背景音樂,如果將這個播放背景音樂的執行緒設定為非守護執行緒,那麼在使用者請求退出的時候,不僅要退出主執行緒,還要通知播放背景音樂的執行緒退出;如果設定為守護執行緒則不需要了。

4.6、停止執行緒

1)概述

Thread.stop()、Thread.suspend、Thread.resume、Runtime.runFinalizersOnExit這些終止執行緒執行的方法已經被廢棄了,使用它們是極端不安全的。

正確停止執行緒的方法:

第一:正常執行完run方法,然後結束掉。

第二:控制迴圈條件和判斷條件的識別符號來結束掉執行緒。

2)實現程式碼示例

class MyThread extends Thread {  
    int i=0;  
    boolean next=true;  
    @Override  
    public void run() {  
        while (next) {  
            if(i==10)  
                next=false;  
            i++;  
            System.out.println(i);  
        }  
    }  
}

4.7、執行緒打斷---interrupt

1)什麼是中斷(interrupt)

​ 中斷只是一種協作機制,Java沒有給中斷增加任何語法,中斷的過程完全需要程式設計師自己實現

​ 每個執行緒物件中都有一個標識,用於表示執行緒是否被中斷;該標識位為true表示中斷,為false表示未中斷;

​ 通過呼叫執行緒物件的interrupt方法將該執行緒的標識位設為true;可以在別的執行緒中呼叫,也可以在自己的執行緒中呼叫。

打斷標記:執行緒是否被打斷,true表示被打斷了,false表示沒有

2)涉及方法

isInterrupted()方法:

獲取執行緒的打斷標記(哪個執行緒物件呼叫就檢查誰的) ,呼叫後不會修改執行緒的打斷標記

interrupt()方法:

中斷this執行緒(哪個執行緒物件呼叫即中斷誰)。如果這個需要被中斷執行緒處於阻塞狀態(sleep、wait、join),那麼它的中斷狀態就會被清除,並且丟擲異常(InterruptedException)。這個中斷並非真正的停止掉執行緒,而是將它的中斷狀態設定成“停止”的狀態,執行緒還是會繼續執行,至於怎麼停止掉該執行緒,還是要靠我們自己去停止,該方法只是將執行緒的狀態設定成“停止”的狀態,即true。

打斷正常執行緒 ,執行緒不會真正被中斷,但是執行緒的打斷標記為true。

interrupted()方法:

檢查當前執行緒是否被中斷,與上面的interrupt()方法配合一起用。執行緒的中斷狀態將會被這個方法清除,也就是說:如果這個方法被連續成功呼叫兩次,第二次

呼叫將會返回false(除非當前執行緒在第一次呼叫之後和第二次呼叫之前又被中斷了)。

也就是說:呼叫後清空打斷標記 即如果獲取為true 呼叫後打斷標記為false (不常用)

4.8、執行緒堵塞

執行緒的阻塞可以分為好多種,從作業系統層面和java層面阻塞的定義可能不同,但是廣義上使得執行緒阻塞的方式有下面幾種:

1)BIO阻塞,即使用了阻塞式的io流
2)sleep(long time) 讓執行緒休眠進入阻塞狀態
3)a.join() 呼叫該方法的執行緒進入阻塞,等待a執行緒執行完恢復執行
4)sychronized或ReentrantLock 造成執行緒未獲得鎖進入阻塞狀態
5)獲得鎖之後呼叫wait()方法 也會讓執行緒進入阻塞狀態
6)LockSupport.park() 讓執行緒進入阻塞狀態

五、執行緒核心方法總結

5.1、六種執行緒狀態和方法的對應關係

5.2、執行緒核心方法總結

1)Thread類中的核心方法

方法名稱 是否static 方法說明
start() 讓執行緒啟動,進入就緒狀態,等待cpu分配時間片
run() 重寫Runnable介面的方法,執行緒獲取到cpu時間片時執行的具體邏輯
yield() 執行緒的禮讓,使得獲取到cpu時間片的執行緒進入就緒狀態,重新爭搶時間片
sleep(time) 執行緒休眠固定時間,進入阻塞狀態,休眠時間完成後重新爭搶時間片,休眠可被打斷
join()/join(time) 呼叫執行緒物件的join方法,呼叫者執行緒進入阻塞,等待執行緒物件執行完或者到達指定時間才恢復,重新爭搶時間片
isInterrupted() 獲取執行緒的打斷標記,true:被打斷,false:沒有被打斷。呼叫後不會修改打斷標記
interrupt() 打斷執行緒,丟擲InterruptedException異常的方法均可被打斷,但是打斷後不會修改打斷標記,正常執行的執行緒被打斷後會修改打斷標記
interrupted() 獲取執行緒的打斷標記。呼叫後會清空打斷標記
stop() 停止執行緒執行 不推薦
suspend() 掛起執行緒 不推薦
resume() 恢復執行緒執行 不推薦
currentThread() 獲取當前執行緒

2)Object中與執行緒相關方法

方法名稱 方法說明
wait()/wait(long timeout) 獲取到鎖的執行緒進入阻塞狀態
notify() 隨機喚醒被wait()的一個執行緒
notifyAll() 喚醒被wait()的所有執行緒,重新爭搶時間片

相關文章