Java多線程-基礎篇

基本概念

進程

  • 正在執行的程序,擁有獨立的代碼和數據空間
  • 進程間的切換會有較大的開銷
  • 是資源分配的最小單位
  • 一個進程可以包含一個或多個線程
  • 至少包含一個線程

線程

  • 程序中單獨順序的控制流
  • 線程本身依靠進程進行運行,只能用分配給進程的資源和環境
  • 同一類線程共享代碼和數據空間,每個線程有獨立的運行棧和程序計數器(PC)
  • 是cpu調度的最小單位

單線程

  • 進程中只存在一個線程,實際上主方法就是一個主線程

多線程

  • 一個進程中運行多個線程
  • 目的:更好的使用CPU資源

並行

  • 真正的同時,多個cpu實例或者多臺機器同時執行一段處理邏輯。

併發

  • 通過cpu調度算法,讓用戶看上去同時執行,實際上從cpu操作層面不是真正的同時。使用TPS或者QPS來反應這個系統的併發處理能力

線程安全

  • 併發的情況之下,該代碼經過多線程使用,線程的調度順序不影響任何結果

同步

  • 保證共享資源的多線程訪問成爲線程安全

死鎖

  • 兩個線程或兩個以上線程都在等待對方執行完畢才能繼續往下執行的時候就發生了死鎖
  • 結果就是這些線程都陷入了無限的等待中

常見線程名詞

主線程
JVM調用程序main()所產生的線程。

當前線程
無特別說明,一般指通過Thread.currentThread()來獲取的進程。

後臺線程

  1. 爲其他線程提供服務的線程,也稱爲守護線程。
  2. 例如,JVM的垃圾回收線程就是一個後臺線程。
  3. 區別在於,守護線程等待主線程,依賴於主線程結束而結束。

前臺線程

  1. 接受後臺線程服務的線程。
  2. 前臺後臺線程關係就像傀儡和幕後操縱者一樣的關係。傀儡是前臺線程、幕後操縱者是後臺線程。
  3. 由前臺線程創建的線程默認也是前臺線程。可以通過isDaemon()和setDaemon()方法來判斷和設置一個線程是否爲後臺線程。

Java中線程基礎知識

  1. main()方法也是一個線程,Thread.currentThread().getName() 得到的值爲main,所有俗稱main線程。
  2. Java中,每次程序至少啓動2個線程。一個main線程,一個是垃圾收集線程。
  3. 每當使用Java命令執行一個類的時候,實際上都會啓動一個JVM,而每一個JVM就是在操作系統中啓動了一個進程。(待驗證
  4. 每個對象都有一個鎖來控制同步訪問。Synchronized關鍵字可以和對象的鎖交互,來實現線程的同步。

線程的實現

繼承java.lang.Thread類

public class MyThread01 extends Thread{
    // 自定義線程名稱
    private String name;
    public MyThread01(String name){
        this.name = name;
    }

    @Override
    public void run() {
        super.run();
        for (int i = 0; i < 1000; i++) {
            System.out.println(name+":"+i);
        }
    }

    public static void main(String[] args) {
        MyThread01 t1 = new MyThread01("A");
        MyThread01 t2 = new MyThread01("B");
        // 此時程序依然是順序執行
        //t1.run();
        //t2.run();

        // System.out.println("----------------華麗分割線----------------");

        // 通過start()方法啓動線程,此時t1、t2線程交替執行
        t1.start();
        t2.start();
    }
}

說明

  1. 程序啓動運行main()時候,Java虛擬機啓動一個進程,主線程在main()方法調用時候被創建。
  2. 隨着調用t1、t2的start()方法,另外兩個線程也啓動了,這樣,整個應用就在多線程下環境下運行了。
  3. 調用start()方法並不會立刻執行線程的代碼,而是使該線程變爲可運行狀態(後面線程生命週期章節會有講解),什麼時候執行是由操作系統決定的。
  4. 線程的啓動是通過start()方法,且不能重複調用。直接通過對象調用run()方法程序依然是順序執行。
  5. 多運行幾次代碼,你會發現,多線程的執行順序是不固定的,每次執行哪個線程是隨機的。
  6. 查看Thread類的源碼,可以發現Thread類是Runable接口的一個實現類。

實現Runable接口

public class MyRunable02 implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }

    public static void main(String[] args) {
        MyRunable02 r1 = new MyRunable02();

        Thread t1 = new Thread(r1);
        Thread t2 = new Thread(r1);

        // 通過start()方法啓動線程,此時t1、t2線程交替執行
        t1.start();
        t2.start();
    }
}

說明

  1. 通過實現Runable接口,使普通的Java類具有了多線程的特性。
  2. run()方法是多線程程序的一個約定,所有多線程的代碼都在run()方法裏面。
  3. 仔細觀察代碼可以發現,最終線程的啓動還是通過Thread類的start()方法。
  4. 實際上Java中所有多線程代碼都是通過Thread.start()方法來啓動的。因此,熟悉Thread類的API是Java併發編程的基礎。

推薦使用Runable的方式

  1. 避免Java單繼承的限制。
  2. 降低數據與代碼的耦合度,代碼與數據獨立,代碼邏輯可被多個線程共享。
  3. 線程池不能直接放入Thread類對象,可以放入Runable、Callable對象。

實現Callable接口

  1. 這裏先簡單普及一下,在Java中Runnable的run()方法沒有返回值,而Callable接口裏的call()方法可返回值。
  2. Java常用Future接口來代表Callable接口裏的call()方法的返回值,併爲Future接口提供了一個FutureTask實現類。
  3. FutureTask類同時實現了Future、Runnable接口。

併發執行同一個FutureTask

public class MyCallable03 implements Callable<String> {
    private int num = 5;
    @Override
    public String call() throws Exception {
        int sum = 0;
        for (int i = 0; i < 10; i++) {
            sum+=i;
            System.out.println(Thread.currentThread().getName()+": "+sum);
        }
		
		// 雖然是併發執行,但是最終只會執行其中一個
        if("Thread-0".equals(Thread.currentThread().getName())){
            this.num = 0; // 此處this就是c1
            System.out.println("now Thread: Thread-0");
        }else if("Thread-1".equals(Thread.currentThread().getName())){
            this.num=10; // 此處this就是c1
            System.out.println("now Thread: Thread-1");
        }
        return Thread.currentThread().getName()+" result:"+sum;
    }

    public static void main(String[] args) throws Exception {

        MyCallable03 c1 = new MyCallable03();
        FutureTask<String> f1 = new FutureTask<String>(c1);

		// t1、t2併發執行
        Thread t1 = new Thread(f1);
        Thread t2 = new Thread(f1);

        t1.start();
        t2.start();

        System.out.println(f1.isDone()); // f1執行完畢纔是true

        Thread.sleep(2000);  //  main線程sleep,保證t1、t2執行完畢
        if(f1.isDone()){
            System.out.println("結果: "+f1.get());  // 結果: Thread-1 result:45
            System.out.println(t1.getState().toString());  // TERMINATED
            System.out.println(t2.getState().toString());  // TERMINATED
        }

        System.out.println("num:"+c1.num);  // 執行的t1爲num:0,執行的t2爲num:10
    }
}

說明

  1. 由t1、t2的狀態得出,兩個線程都執行了。
  2. 根據num的值與輸出的“now Thread: Thread-?”結果得出,一個FutureTask只會被執行一次。

先後執行同一個FutureTask

public class MyCallable0302 implements Callable<String> {

    private static int num = 5;
    
    @Override
    public String call() throws Exception {
        int sum = 0;
        for (int i = 0; i < 10; i++) {
            sum+=i;
            System.out.println(Thread.currentThread().getName()+": "+sum);
        }
        
        // 最終只會執行其中一個
        if(Thread.currentThread().getName().equals("Thread-0")){
            this.num = 0; // 此處this就是c1
            System.out.println("now Thread: Thread-0");
        }else if("Thread-1".equals(Thread.currentThread().getName())){
            this.num=10; // 此處this就是c1
            System.out.println("now Thread: Thread-1");
        }
        
        return Thread.currentThread().getName()+" result:"+sum;
    }

    public static void main(String[] args) throws Exception {

        MyCallable0302 c1 = new MyCallable0302();
        FutureTask<String> f1 = new FutureTask<String>(c1);

        Thread t1 = new Thread(f1);

        t1.start();

        // 此時會繼續執行主線程的代碼。
        System.out.println(f1.isDone()); // f1執行完畢纔是true

        Thread.sleep(2000); //  main線程sleep,保證t1執行完畢
        if(f1.isDone()){
            System.out.println("第一次: "+f1.get());  // 第一次: Thread-0 result:45
            System.out.println("num:"+c1.num); // num:0
            System.out.println(t1.getState().toString());  // TERMINATED

            Thread t2 = new Thread(f1);// 此時,f1中的call方法都不會被執行,相當於沒傳f1
            t2.start();
            Thread.sleep(2000); // main線程sleep,保證t2執行完畢
            System.out.println("第二次: "+f1.get());  // 第二次: Thread-0 result:45(依然是t1執行f1的結果)
            System.out.println(t2.getState().toString()); // TERMINATED
        }

        System.out.println("num:"+c1.num); // num:0 (依然是t1執行f1的結果)

        // 第二個線程執行第一個線程已經執行完的f1,第二個線程會執行,但是不是執行f1中的call方法
    }
}

說明

  1. 由兩個“依然是t1執行f1的結果”進一步驗證了:一個FutureTask只會被執行一次。

線程中常用方法

方法名 作用 詳解
getName() 獲取線程名稱 Thread.currentThread().getName()
currentThread() 獲取當前線程對象 currentThread()方法是Thread類的靜態方法,如果調用線程對象.currentThread()方法並不能獲取到調用的線程對象,反正在哪一個線程裏面執行了currentThread()方法得到的就是哪個線程對象
isAlive() 線程是否存活 如果線程已經啓動,並且沒有died返回true
join() 等待該線程終止 在一個線程中調用other.join(),將等待other執行完後才繼續本線程。(見補充說明)
sleep() 線程的休眠 見補充說明
yield() 線程禮讓 當前線程可轉讓cpu控制權,讓別的就緒狀態線程運行(見補充說明)
interrupte() 友好的終止線程執行 保證程序邏輯完整性(見補充說明)
wait() 線程掛起,進入等待隊列 JAVA多線程-Object.wait(),Object.notify(),Object.notifyAll()
notify() 喚醒等待隊列中任意一個線程 JAVA多線程-Object.wait(),Object.notify(),Object.notifyAll()
notifyAll() 喚醒等待隊列中所有線程 JAVA多線程-Object.wait(),Object.notify(),Object.notifyAll()
suspend() 線程掛起 不會釋放對象鎖,不推薦使用,常與resume()配套使用
resume() 喚醒掛起線程 不推薦使用,常與suspend()配套使用。如果 resume() 操作出現在 suspend() 之前執行,很容易造成死鎖
activeCount() 進程中活躍的線程數
enumerate() 枚舉程序中的線程
isDaemon() 一個線程是否爲守護線程
setDaemon() 設置一個線程爲守護線程 用戶線程和守護線程的區別在於,是否等待主線程依賴於主線程結束而結束
setPriority() 設置一個線程的優先級 取值1-10

補充說明

join()
這裏是指的主線程等待子線程的終止。如果還要其他線程的話,調用join()方法的線程會與除主線程外的其他線程併發執行。所以,當主線程需要用到子線程的處理結果,這個時候就要用到join()方法。

yield()
讓當前運行線程從運行狀態(Running)回到可運行狀態(Runable),以允許具有相同優先級的其他線程獲得運行機會。因此,使用yield()的目的是讓相同優先級的線程之間能適當的輪轉執行。但是,實際中無法保證yield()達到讓步目的,因爲讓步的線程還有可能被線程調度程序再次選中

sleep()

  1. 線程休眠,不會釋放鎖
  2. sleep()方法是Thread類的靜態方法,如果調用線程對象.sleep()方法並不是該線程就休眠,反正在哪一個線程裏面執行了sleep()方法哪一個線程就休眠。
  3. 在sleep()休眠時間期滿後,該線程不一定會立刻獲得cpu資源,除非此線程具有更高的優先級。

sleep()、yield()的區別

  1. sleep()使當前線程進入阻塞狀態,所以執行sleep()的線程在指定的時間內肯定不會被執行。這段時間是通過程序設定的。
  2. yield()只是使當前線程重新回到可執行狀態,所以執行yield()的線程可能進入到可執行狀態後又馬上被執行,這段時間是不可設定的。
  3. 實際上,yield()方法對應瞭如下操作:先檢測當前是否有相同優先級的線程處於同可運行狀態,如有,則把 CPU 的佔有權交給此線程,否則,繼續運行原來的線程。所以yield()方法稱爲“退讓”,它把運行機會讓給了同等優先級的其他線程。
  4. sleep() 方法允許較低優先級的線程獲得運行機會,但 yield() 方法執行時,當前線程仍處在可運行狀態,所以,不可能讓出較低優先級的線程些時獲得 CPU 佔有權。所以,如果較高優先級的線程沒有調用 sleep 方法,又沒有受到 I\O 阻塞,那麼,較低優先級線程只能等待所有較高優先級的線程運行結束,纔有機會運行

interrupte()
不要以爲它是中斷某個線程!它只是向線程發送一箇中斷信號。正常運行的程序不去檢測狀態就不會終止。
只會影響到wait狀態、sleep狀態和join狀態。被打斷的線程會拋出InterruptedException。
stop()
是一種"惡意" 的中斷,一旦執行stop方法,即終止當前正在運行的線程,不管線程邏輯是否完整,這是非常危險的。
綜合interrupte()、stop(),建議使用自定義的標誌位決定線程的執行情況

class SafeStopThread extends Thread {
    //此變量必須加上volatile
    private volatile boolean stop = true;
    @Override
    public void run() {
        //判斷線程體是否運行
        while (stop) {
            // Do Something
            System.out.println("Stop");
        }
    }
    //線程終止
    public void terminate() {
        stop = false;
    }
}

線程優先級

  • Java線程的優先級用整數表示,取值範圍是1~10,數值越大優先級越高。優先級高的線程獲得更多運行機會的機會越大。
  • Thread類的setPriority()和getPriority()方法分別用來設置和獲取線程的優先級。
  • 線程的優先級有繼承關係,比如A線程中創建了B線程,那麼B將和A具有相同的優先級。
  • JVM提供了10個線程優先級,但與常見的操作系統都不能很好的映射。如果希望程序能移植到各個操作系統中,應該僅僅使用Thread類有以下三個靜態常量作爲優先級,這樣能保證同樣的優先級採用了同樣的調度方式。
    Thread.MIN_PRIORITY => 1
    Thread.MAX_PRIORITY=> 10
    Thread.NORM_PRIORITY=> 5(默認)
    

說明

  1. 線程的優先級有可能影響線程的執行順序,不是絕對的。

線程同步

有共享數據時就需要同步!!!

同步代碼塊

在代碼塊上加上"synchronized" 關鍵字,則此代碼塊就成爲同步代碼塊

synchronized(同步對象){
          需要同步的代碼塊;
  }

同步方法

在方法返回修飾符之前加上"synchronized" 關鍵字,則此方法就成爲同步方法

synchronized void 方法名(){
	....
}

線程生命週期

  • 新建(New):新建一個線程對象

  • 可運行(Runable)
    其他線程調用該線程的start()方法。不能對同一線程對象兩次調用start()方法。
    該線程位於可運行線程池中,等待獲取cpu使用權。

  • 運行(running):獲取了cpu使用權,執行程序代碼

  • 阻塞(block):因某種原因[]放棄了cpu使用權,暫時停止運行。直到線程再次進入可運行狀態纔有可能獲取cpu使用權,轉爲運行狀態。

    • 等待阻塞:執行了wait()方法。jvm把線程放入等待隊列,釋放鎖。被notify(), notifyAll()進入鎖池中
    • 同步阻塞:獲取同步鎖時,該鎖被別的線程佔用。jvm把線程放入鎖池中
    • 其他阻塞:執行sleep(毫秒)、join方法、或者發出I/O請求。不釋放鎖
  • 死亡(dead):線程執行完成或因異常退出,該線程結束生命週期。死亡的線程不可再次恢復圖片來源網絡

參考
https://blog.csdn.net/Evankaka/article/details/44153709

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