【Java多線程-1】線程概述與線程創建和使用


說到程序,離不開進程和線程這兩個概念。那麼這兩者分別有什麼作用和區別呢?

1 線程概述

進程是表示資源分配的基本單位,又是調度運行的基本單位。例如,用戶運行自己的程序,系統就創建一個進程,併爲它分配資源,包括各種表格、內存空間、磁盤空間、I/O設備等。然後,把該進程放人進程的就緒隊列。進程調度程序選中它,爲它分配CPU以及其它有關資源,該進程才真正運行。所以,進程是系統中的併發執行的單位。如下圖所示,在 windows 中通過查看任務管理器的方式,我們就可以清楚看到 window 當前運行的進程(.exe文件的運行)。
在這裏插入圖片描述
線程是進程中執行運算的最小單位,亦即執行處理機調度的基本單位。如果把進程理解爲在邏輯上操作系統所完成的任務,那麼線程表示完成該任務的許多可能的子任務之一。例如,假設用戶啓動了一個窗口中的數據庫應用程序,操作系統就將對數據庫的調用表示爲一個進程。假設用戶要從數據庫中產生一份工資單報表,並傳到一個文件中,這是一個子任務;在產生工資單報表的過程中,用戶又可以輸人數據庫查詢請求,這又是一個子任務。這樣,操作系統則把每一個請求――工資單報表和新輸人的數據查詢表示爲數據庫進程中的獨立的線程。線程可以在處理器上獨立調度執行,這樣,在多處理器環境下就允許幾個線程各自在單獨處理器上進行。操作系統提供線程就是爲了方便而有效地實現這種併發性。

一個cpu在任意時刻,只能執行一個線程任務,我們平時一邊瀏覽網頁,一邊聽音樂的場景其實是依賴cpu高速地線程間切換實現地,這也引出了一組容易混淆地概念:併發(Concurrency)和並行(Parallelism)
它們都可以表示兩個或者多個任務一起執行,但是偏重點有些不同。併發偏重於多個任務交替執行,而多個任務之間有可能還是串行的,而並行是真正意義上的“同時執行”,它依賴於多核cpu而實現。

引入線程的好處:

  • 易於調度。
  • 提高併發性。通過線程可方便有效地實現併發性。進程可創建多個線程來執行同一程序的不同部分。
  • 開銷少。創建線程比創建進程要快,所需開銷很少。
  • 利於充分發揮多處理器的功能。通過創建多線程進程(即一個進程可具有兩個或更多個線程),每個線程在一個處理器上運行,從而實現應用程序的併發性,使每個處理器都得到充分運行。

進程和線程的關係:

  • 一個線程只能屬於一個進程,而一個進程可以有多個線程,但至少有一個線程。線程是操作系統可識別的最小執行和調度單位。
  • 資源分配給進程,同一進程的所有線程共享該進程的所有資源。 同一進程中的多個線程共享代碼段(代碼和常量),數據段(全局變量和靜態變量),擴展段(堆存儲)。但是每個線程擁有自己的棧段,棧段又叫運行時段,用來存放所有局部變量和臨時變量。
  • 處理機分給線程,即真正在處理機上運行的是線程。
  • 線程在執行過程中,需要協作同步。不同進程的線程間要利用消息通信的辦法實現同步。

1.1 線程生命週期

線程是一個動態執行的過程,它也有一個從產生到死亡的過程。
下圖顯示了一個線程完整的生命週期:
在這裏插入圖片描述

  1. 新建狀態:New,使用 new 關鍵字和 Thread 類或其子類建立一個線程對象後,該線程對象就處於新建狀態。它保持這個狀態直到程序 start() 這個線程。
  2. 就緒狀態:Runnable,當線程對象調用了start()方法之後,該線程就進入就緒狀態。就緒狀態的線程處於就緒隊列中,要等待JVM裏線程調度器的調度。
  3. 運行狀態:Running,如果就緒狀態的線程獲取 CPU 資源,就可以執行 run(),此時線程便處於運行狀態。處於運行狀態的線程最爲複雜,它可以變爲阻塞狀態、就緒狀態和死亡狀態。
  4. 阻塞狀態:Blocked,如果一個線程執行了sleep(睡眠)、suspend(掛起)等方法,失去所佔用資源之後,該線程就從運行狀態進入阻塞狀態。在睡眠時間已到或獲得設備資源後可以重新進入就緒狀態。可以分爲三種:
    • 等待阻塞:運行狀態中的線程執行 wait() 方法,使線程進入到等待阻塞狀態。
    • 同步阻塞:線程在獲取 synchronized 同步鎖失敗(因爲同步鎖被其他線程佔用)。
    • 其他阻塞:通過調用線程的 sleep() 或 join() 發出了 I/O 請求時,線程就會進入到阻塞狀態。當sleep() 狀態超時,join() 等待線程終止或超時,或者 I/O 處理完畢,線程重新轉入就緒狀態。
  5. 死亡狀態:Dead,一個運行狀態的線程完成任務或者其他終止條件發生時,該線程就切換到終止狀態。

2 多線程實現方式

Java 提供了三種創建線程的方法:

  • 通過繼承 Thread 類本身。
  • 通過實現 Runnable 接口。
  • 通過 Callable 和 Future 創建線程。

2.1 繼承Thread

創建一個線程的第一種方法是創建一個類並繼承 Thread 類,然後創建一個該類的實例。繼承類必須重寫 run() 方法,該方法是新線程的入口點。它也必須調用 start() 方法才能執行。
請看下面實例:

/**
 * @author guozhengMu
 * @version 1.0
 * @date 2019/11/2 14:31
 * @description
 * @modify
 */
public class ThreadTest {
    public static void main(String[] args) {

        MyThread thread1 = new MyThread("Thread-A");
        MyThread thread2 = new MyThread("Thread-B");
        System.out.println();
        thread1.start();
        thread2.start();
    }
}

class MyThread extends Thread {
    private String name;

    public MyThread(String name) {
        this.name = name;
        System.out.println("創建線程 " + name);
    }

    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println("運行線程 " + name + " " + i);
            // 線程休眠,增強線程交互執行的效果
            try {
                sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("\r\n退出線程 " + name);
    }
}

運行結果如下:

創建線程 Thread-A
創建線程 Thread-B

運行線程 Thread-B 0
運行線程 Thread-A 0
運行線程 Thread-A 1
運行線程 Thread-B 1
運行線程 Thread-A 2
運行線程 Thread-B 2

退出線程 Thread-B

退出線程 Thread-A

多運行幾次,會發現運行線程部分的結果是隨機的,這也印證了多線程執行順序的不確定性。

2.2 實現Runnable

創建一個線程第二種方法是創建一個實現 Runnable 接口的類。

/**
 * @author guozhengMu
 * @version 1.0
 * @date 2019/11/2 14:31
 * @description
 * @modify
 */
public class ThreadTest {
    public static void main(String[] args) {
        MyRunnable runnable1 = new MyRunnable("Thread-A");
        MyRunnable runnable2 = new MyRunnable("Thread-B");
        System.out.println();
        runnable1.start();
        runnable2.start();
    }
}

class MyRunnable implements Runnable {
    private Thread thread;
    private String name;

    public MyRunnable(String name) {
        this.name = name;
        System.out.println("創建線程 " + name);
    }

    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println("運行線程 " + name + " " + i);
            // 線程休眠,增強線程交互執行的效果
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("\r\n退出線程 " + name);
    }

    public void start() {
        System.out.println("啓動線程 " + name);
        if (thread == null) {
            thread = new Thread(this, name);
            thread.start();
        }
    }
}

運行結果:

創建線程 Thread-A
創建線程 Thread-B

啓動線程 Thread-A
啓動線程 Thread-B
運行線程 Thread-B 0
運行線程 Thread-A 0
運行線程 Thread-A 1
運行線程 Thread-B 1
運行線程 Thread-B 2
運行線程 Thread-A 2

退出線程 Thread-A

退出線程 Thread-B

2.3 通過 Callable 和 Future 創建線程

第三種方法是通過 Callable 和 Future 創建線程,前面兩種都是無返回值,而這種方法適合獲取線程執行結果。基本步驟如下:

  1. 創建 Callable 接口的實現類,並實現 call() 方法,該 call() 方法將作爲線程執行體,並且有返回值。
  2. 創建 Callable 實現類的實例,使用 FutureTask 類來包裝 Callable 對象,該 FutureTask 對象封裝了該 Callable 對象的 call() 方法的返回值。
  3. 使用 FutureTask 對象作爲 Thread 對象的 target 創建並啓動新線程。
  4. 調用 FutureTask 對象的 get() 方法來獲得子線程執行結束後的返回值。
/**
 * @author guozhengMu
 * @version 1.0
 * @date 2019/11/2 18:36
 * @description
 * @modify
 */
public class CallableTest implements Callable<Integer> {
    public static void main(String[] args) {
        CallableTest callableTest = new CallableTest();
        FutureTask<Integer> ft = new FutureTask<>(callableTest);

        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + " 的變量i的值" + i);
            if (i == 5) {
                new Thread(ft, "有返回值的線程").start();
            }
        }
        try {
            System.out.println("子線程的返回值:" + ft.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

    }

    @Override
    public Integer call() {
        int i = 0;
        for (; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + "-" + i);
        }
        return i;
    }
}

輸出結果:

main 的變量i的值0
main 的變量i的值1
main 的變量i的值2
main 的變量i的值3
main 的變量i的值4
main 的變量i的值5
main 的變量i的值6
main 的變量i的值7
main 的變量i的值8
main 的變量i的值9
有返回值的線程-0
有返回值的線程-1
有返回值的線程-2
有返回值的線程-3
有返回值的線程-4
有返回值的線程-5
有返回值的線程-6
有返回值的線程-7
有返回值的線程-8
有返回值的線程-9
子線程的返回值:10

3 線程的控制(常見方法)

下表列出了Thread類的一些重要方法:

方法 描述
public void run() 使該線程開始執行;Java 虛擬機調用該線程的 run 方法。
public void start() 使該線程開始執行;Java 虛擬機調用該線程的 run 方法。
public final void setName(String name) 改變線程名稱,使之與參數 name 相同。
public final void setPriority(int priority) 更改線程的優先級。
public final void setDaemon(boolean on) 將該線程標記爲守護線程或用戶線程。
public final void join(long millisec) 等待該線程終止的時間最長爲 millis 毫秒。
public void interrupt() 中斷線程。
public final boolean isAlive() 測試線程是否處於活動狀態。
public static void yield() 讓當前運行線程回到可運行狀態,以允許具有相同優先級的其他線程獲得運行機會。
public static void sleep(long millisec) 在指定的毫秒數內讓當前正在執行的線程休眠(暫停執行),此操作受到系統計時器和調度程序精度和準確性的影響。
public static Thread currentThread() 返回對當前正在執行的線程對象的引用。
public static boolean holdsLock(Object x) 當且僅當當前線程在指定的對象上保持監視器鎖時,才返回 true。
public static void dumpStack() 將當前線程的堆棧跟蹤打印至標準錯誤流。

一些重要方法的使用實例待完善…

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