本文旨在用最通俗的語言講述最枯燥的基本知識。
全文提綱:
1.執行緒是什麼?(上)
2.執行緒和程式的區別和聯絡(上)
3.建立多執行緒的方法(上)
4.執行緒的生命週期(上)
5.執行緒的控制(上)
6.執行緒同步(下)
7.執行緒池(下)
8.ThreadLocal的基本用法(下)
9.執行緒安全(下)
1.執行緒是什麼
執行緒是程式中的一個執行流程,是被系統獨立排程和分派的基本單位。
執行緒是什麼?程式是什麼?
這麼說可能有點懵逼,舉個栗子吧:
A工廠是一個生產汽車的工廠,今天員工張三去送貨,員工李四去進貨。這裡的
- A工廠是一個程式(當然荒廢的沒有生命跡象的工廠不能算程式了)
- 員工張三去送貨 是一個執行緒
- 員工李四去進貨 也是一個執行緒
從例子可以看出
程式是指執行中的程式(沒執行的程式,系統是不會為之分配資源的),每個程式都有自己獨立的記憶體空間,當一個程式進入記憶體執行時,程式內部可能包含多個程式執行流,這個程式執行流就是一個執行緒。
幾乎所有作業系統都支援多執行緒併發,就像我們平時上班用的電腦,我們可能習慣開啟eclipse寫程式碼,同時開啟網易雲音樂聽課,而且還要開啟有道翻譯時刻準備著把我們的中文轉成英文…
可見我們的電腦可以支援多個應用程式同時執行的,但實際上,而對於每個CPU來說,它在一個時間點內,只能執行一個程式,也就是一個程式,那為什麼我們同時開啟這麼多程式執行沒問題呢?
那是因為現代電腦都不止一個CPU啦。當然這個是一個原因。
最主要的是因為在程式執行過程中,CPU在不同程式之間高速的來回切換執行,因此所謂的“併發執行”實際上並不是多個程式在同時執行,而是系統對程式的執行做了排程,讓視覺上看起來是同時執行了。
所以執行緒中的併發:
是指多個程式被CPU快速的輪換執行,而不是同時執行
2. 執行緒和程式的區別
通過上面的原理講述已經能看出區別了,最主要有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介面,它有以下幾個方法:
- boolean cancal(boolean mayInterruptRunning):試圖取消該Future裡關聯的Callable任務
- V get():返回Callable任務裡call()方法的返回值,呼叫該方法將導致程式阻塞,必須等到子執行緒結束後才會得到返回值
- V get(long timeout,TimeUnit unit):
- boolean isCancel():如果在Callable任務正常完成前被取消,則返回true
- 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資源,執行緒之間的切換也就無從談起了。因此,執行緒是有生命週期的,他的生命週期包括以下幾種狀態:
- 新建(NEW)
- 就緒(Runnable)
- 執行(Running)
- 阻塞(Blocked)
- 死亡(Dead)
1. 新建狀態
當在程式中用new建立一個執行緒之後,它就處於新建狀態,此時它和程式中其它物件一樣處於初始化狀態(分配記憶體、初始化成員變數)。
2.就緒狀態
當程式呼叫了start方法之後,程式就處於就緒狀態,jvm會為它建立方法呼叫棧和程式計數器,此時的執行緒狀態為可執行狀態,並沒有執行,而是需要執行緒排程器的排程決定何時執行。
3. 執行狀態
當就緒的執行緒獲得CPU之後,就會執行執行緒執行體(run方法),這時候執行緒就處於了執行狀態。
4.阻塞狀態
處於執行的狀態的執行緒,除非執行時間非常非常非常短,否則它會因為系統對資源的排程而被中斷進入阻塞狀態。作業系統大多采用的是搶佔式排程策略,線上程獲得CPU之後,系統給執行緒一段時間來處理任務,當到時間之後,系統會強制性剝奪執行緒所佔資源,而分配別的執行緒,至於分配給誰,這個取決於執行緒的優先順序。
5.死亡狀態
處於執行狀態的執行緒,當它主動或者被動結束,執行緒就處於死亡狀態。至於結束的形式,通常有以下幾種:
- 執行緒執行完成,執行緒正常結束
- 執行緒執行過程中出現異常或者錯誤,被動結束
- 執行緒主動呼叫stop方法結束執行緒
5.執行緒的控制
Java提供了執行緒在其生命週期中的一些方法,便於開發者對執行緒有更好的控制。
主要有以下方法:
- 等 待:join()
- 後 臺:setDeamon()
- 睡 眠:sleep()
- 讓 步:yield()
- 優先順序: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。
關於多執行緒的揭祕,上集都講到這裡,更高階的使用多執行緒,盡在下集 請關注我哦。
覺得本文對你有幫助?請分享給更多人
關注「程式設計無界」,提升裝逼技能