Java多線程全揭祕


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

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


1.線程是什麼

線程是進程中的一個執行流程,是被系統獨立調度和分派的基本單位。

線程是什麼?進程是什麼?
這麼說可能有點懵逼,舉個栗子吧:

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

  1. A工廠是一個進程(當然荒廢的沒有生命跡象的工廠不能算進程了)

  2. 員工張三去送貨 是一個線程

  3. 員工李四去進貨 也是一個線程

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

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

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資源,線程之間的切換也就無從談起了。因此,線程是有生命週期的,他的生命週期包括以下幾種狀態:

  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。

關於多線程的揭祕,上集都講到這裏,更高級的使用多線程,盡在下集 請關注我哦。





1Redis模糊查詢在生產環境出現嚴重性能問題


2Linux性能的重要指標:打開文件數的限制


3以Vue爲例,解釋JavaScript的反應性


4Java必知必會之----Enum枚舉類揭祕


518位×××號藏什麼玄機?用js教你校驗





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

關注「編程×××」,提升裝逼技能




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章