Java必知必會之(四)---多執行緒全揭祕(上)

假不理發表於2018-09-04

本文旨在用最通俗的語言講述最枯燥的基本知識。

全文提綱:
1.執行緒是什麼?(上)
2.執行緒和程式的區別和聯絡(上)
3.建立多執行緒的方法(上)
4.執行緒的生命週期(上)
5.執行緒的控制(上)
6.執行緒同步(下)
7.執行緒池(下)
8.ThreadLocal的基本用法(下)
9.執行緒安全(下)


1.執行緒是什麼

執行緒是程式中的一個執行流程,是被系統獨立排程和分派的基本單位。

執行緒是什麼?程式是什麼?
這麼說可能有點懵逼,舉個例子吧:

A工廠是一個生產汽車的工廠,今天員工張三去送貨,員工李四去進貨。這裡的

  1. A工廠是一個程式(當然荒廢的沒有生命跡象的工廠不能算程式了)
  2. 員工張三去送貨 是一個執行緒
  3. 員工李四去進貨 也是一個執行緒

從例子可以看出
程式是指執行中的程式(沒執行的程式,系統是不會為之分配資源的),每個程式都有自己獨立的記憶體空間,當一個程式進入記憶體執行時,程式內部可能包含多個程式執行流,這個程式執行流就是一個執行緒。

幾乎所有作業系統都支援多執行緒併發,就像我們平時上班用的電腦,我們可能習慣開啟eclipse寫程式碼,同時開啟網易雲音樂聽課,而且還要開啟有道翻譯時刻準備著把我們的中文轉成英文…
可見我們的電腦可以支援多個應用程式同時執行的,但實際上,而對於每個CPU來說,它在一個時間點內,只能執行一個程式,也就是一個程式,那為什麼我們同時開啟這麼多程式執行沒問題呢?
那是因為現代電腦都不止一個CPU啦。當然這個是一個原因。
最主要的是因為在程式執行過程中,CPU在不同程式之間高速的來回切換執行,因此所謂的“併發執行”實際上並不是多個程式在同時執行,而是系統對程式的執行做了排程,讓視覺上看起來是同時執行了。
所以執行緒中的併發:
是指多個程式被CPU快速的輪換執行,而不是同時執行

2. 執行緒和程式的區別

通過上面的原理講述已經能看出區別了,最主要有2點

  1. 執行緒作為排程和分配的基本單位,程式作為擁有資源的基本單位
  2. 執行緒是程式中的一個執行流程,一個程式可以包含多個執行緒

3. 多執行緒的建立

1. 繼承Thread類建立執行緒:

 1public class ThreadTest  extends Thread {
2    @Override
3    public void run() {
4        // 業務邏輯
5        super.run();
6    }
7
8    public static void main(String[] args) {
9        new ThreadTest().run();
10        new ThreadTest().run();
11    }
12}
複製程式碼

2. 實現Runnable介面

 1public class ThreadTest  implements Runnable {
2    @Override
3    public void run() {
4         //業務邏輯
5    }
6
7    public static void main(String[] args) {
8        new ThreadTest().run();
9        new ThreadTest().run();
10    }
11}
複製程式碼

3. 使用Callable和Future建立

Callable介面是jdk5之後的新介面,它提供了一個call方法作為執行緒執行體,和thread的run方法類似,但是它的功能更強大:

  • 它可以有返回值
  • 它可以宣告丟擲異常
    因此也可以像Runnable一樣,建立一個Callable物件作為Thread的target,而實現它的call方法作為執行體.
    同時jdk5提供了Future介面來代表Callable介面裡的call方法返回值,併為Future介面提供了一個FutureTask實現類,該實現類實現了Future介面,並實現了Runable介面,它有以下幾個方法:
  1. boolean cancal(boolean mayInterruptRunning):試圖取消該Future裡關聯的Callable任務
  2. V get():返回Callable任務裡call()方法的返回值,呼叫該方法將導致程式阻塞,必須等到子執行緒結束後才會得到返回值
  3. V get(long timeout,TimeUnit unit):
  4. boolean isCancel():如果在Callable任務正常完成前被取消,則返回true
  5. boolean isDone():如果Callable任務已完成,則返回true
 1public static void main(String[] args) {
2
3        //1)建立一個Callable實現類,並實現call方法
4        //2)用FutrueTask來包裝類的例項
5     FutureTask ft=new FutureTask<>(new Callable<Integer>() {
6        @Override
7        public Integer call() throws Exception {
8            System.out.println("執行了");
9            try {
10                Thread.sleep(1000*5);
11            } catch (InterruptedException e1) {
12                // TODO Auto-generated catch block
13                e1.printStackTrace();
14            }
15
16            return 12;
17        }
18    });
19     //使用FutureTask物件作為target來建立並且啟動執行緒
20     new Thread(ft).start();
21
22     //阻塞方式獲取執行緒返回值
23     try {
24        System.out.println("返回值:"+ft.get());
25    } catch (InterruptedException e) {
26        // TODO Auto-generated catch block
27        e.printStackTrace();
28    } catch (ExecutionException e) {
29        // TODO Auto-generated catch block
30        e.printStackTrace();
31    }
32     //帶有超時方式獲取執行緒返回值
33     try {
34        System.out.println("返回值:"+ft.get(2,TimeUnit.SECONDS));
35    } catch (InterruptedException | ExecutionException | TimeoutException e) {
36        // TODO Auto-generated catch block
37        e.printStackTrace();
38    }
39
40    }
複製程式碼

4. 執行緒的生命週期

執行緒建立之後,不會立即處於執行狀態,根據前面對併發的定義理解:即使他啟動了也不會永遠都處於執行狀態,如果它一直處於執行狀態,就會一直佔據著CPU資源,執行緒之間的切換也就無從談起了。因此,執行緒是有生命週期的,他的生命週期包括以下幾種狀態:

Java必知必會之(四)---多執行緒全揭祕(上)

  1. 新建(NEW)
  2. 就緒(Runnable)
  3. 執行(Running)
  4. 阻塞(Blocked)
  5. 死亡(Dead)
1. 新建狀態

當在程式中用new建立一個執行緒之後,它就處於新建狀態,此時它和程式中其它物件一樣處於初始化狀態(分配記憶體、初始化成員變數)。

2.就緒狀態

當程式呼叫了start方法之後,程式就處於就緒狀態,jvm會為它建立方法呼叫棧和程式計數器,此時的執行緒狀態為可執行狀態,並沒有執行,而是需要執行緒排程器的排程決定何時執行。

3. 執行狀態

當就緒的執行緒獲得CPU之後,就會執行執行緒執行體(run方法),這時候執行緒就處於了執行狀態。

4.阻塞狀態

處於執行的狀態的執行緒,除非執行時間非常非常非常短,否則它會因為系統對資源的排程而被中斷進入阻塞狀態。作業系統大多采用的是搶佔式排程策略,線上程獲得CPU之後,系統給執行緒一段時間來處理任務,當到時間之後,系統會強制性剝奪執行緒所佔資源,而分配別的執行緒,至於分配給誰,這個取決於執行緒的優先順序。

5.死亡狀態

處於執行狀態的執行緒,當它主動或者被動結束,執行緒就處於死亡狀態。至於結束的形式,通常有以下幾種:

  1. 執行緒執行完成,執行緒正常結束
  2. 執行緒執行過程中出現異常或者錯誤,被動結束
  3. 執行緒主動呼叫stop方法結束執行緒

5.執行緒的控制

Java提供了執行緒在其生命週期中的一些方法,便於開發者對執行緒有更好的控制。
主要有以下方法:

  1. 等 待:join()
  2. 後 臺:setDeamon()
  3. 睡 眠:sleep()
  4. 讓 步:yield()
  5. 優先順序:setPriority()

1.執行緒等待

當某個執行緒執行流中呼叫其他執行緒的join()方法時,呼叫執行緒將被阻塞,知道被join()方法加入的join()執行緒執行完成為止。

乍一看,怎麼也理解不了,這句話,我們來寫一個程式測試一下:

 1public class ThreadTest  extends Thread {
2    @Override
3    public void run() {
4        System.out.println(getName()+"執行...");
5        for(int i=0;i<5;i++){
6            System.out.println(getName()+"執行:"+i);
7        }
8    }
9    public ThreadTest(String name){
10        super(name);
11    }
12    public static void main(String[] args) {
13        //main方法--主執行緒
14        //執行緒1
15        new ThreadTest("子執行緒1").start();
16        //執行緒2
17        ThreadTest t2=new ThreadTest("子執行緒2");
18        t2.start();
19
20        try {
21          t2.join(1000);
22        } catch (InterruptedException e) {
23          e.printStackTrace();
24        }
25        //執行緒3
26        new ThreadTest("子執行緒3").start();
27    }
28}
複製程式碼

看輸出結果:

 1子執行緒1執行...
2子執行緒2執行...
3子執行緒2執行:0
4子執行緒2執行:1
5子執行緒2執行:2
6子執行緒2執行:3
7子執行緒1執行:0
8子執行緒2執行:4
9子執行緒1執行:1
10子執行緒1執行:2
11子執行緒1執行:3
12子執行緒1執行:4
13子執行緒3執行...
14子執行緒3執行:0
15子執行緒3執行:1
16子執行緒3執行:2
17子執行緒3執行:3
18子執行緒3執行:4
複製程式碼

可以看到,執行緒1和2在併發執行著,而執行緒3則在他們都執行完之後才開始。
由此可知:
join()方法呼叫之後,後面的執行緒必須等待前面執行完之後才能執行,而不是併發執行

2.執行緒轉入後臺

當執行緒呼叫了setDaemon(true)之後,它就轉入為後臺執行緒,為前臺執行緒提供服務,而當前臺所有執行緒死亡時,後臺執行緒也會接受到JVM的通知而自動死亡。

1ThreadTest t2=new ThreadTest("子執行緒2");
2//這是為後臺執行緒,但必須在start前設定,因為前臺執行緒死亡JVM會通知
3//後臺執行緒死亡,但接受指令到響應需要時間。因此要自愛start前就設定
4        t2.setDaemon(true);
5        t2.start();
複製程式碼

3. 執行緒睡眠

當需要某個處於執行狀態的執行緒暫停執行並且進入阻塞狀態時,呼叫Thread.sleep既可。

4.執行緒讓步

當需要某個處於執行狀態的執行緒暫停執行並且進入就緒狀態,呼叫
Thread.yield()即可

5.執行緒優先順序

前面說到,系統分配CPU給哪個執行緒的執行,取決於執行緒的優先順序,因此每個執行緒都有一定的優先順序,優先順序高的執行緒會獲得更多的執行機會,預設情況下,每個執行緒的預設優先順序都與建立它的父執行緒優先順序一致。
當我們需要某個執行緒或者更多的執行機會時,呼叫
Thread.currentThread().setPriority(int newPriority);
方法即可,newPriority的範圍在1~10。

關於多執行緒的揭祕,上集都講到這裡,更高階的使用多執行緒,盡在下集 請關注我哦。


覺得本文對你有幫助?請分享給更多人

關注「程式設計無界」,提升裝逼技能

Java必知必會之(四)---多執行緒全揭祕(上)

相關文章