線程概述與線程創建和使用
說到程序,離不開進程和線程這兩個概念。那麼這兩者分別有什麼作用和區別呢?
1 線程概述
進程是表示資源分配的基本單位,又是調度運行的基本單位。例如,用戶運行自己的程序,系統就創建一個進程,併爲它分配資源,包括各種表格、內存空間、磁盤空間、I/O設備等。然後,把該進程放人進程的就緒隊列。進程調度程序選中它,爲它分配CPU以及其它有關資源,該進程才真正運行。所以,進程是系統中的併發執行的單位。如下圖所示,在 windows 中通過查看任務管理器的方式,我們就可以清楚看到 window 當前運行的進程(.exe文件的運行)。
線程是進程中執行運算的最小單位,亦即執行處理機調度的基本單位。如果把進程理解爲在邏輯上操作系統所完成的任務,那麼線程表示完成該任務的許多可能的子任務之一。例如,假設用戶啓動了一個窗口中的數據庫應用程序,操作系統就將對數據庫的調用表示爲一個進程。假設用戶要從數據庫中產生一份工資單報表,並傳到一個文件中,這是一個子任務;在產生工資單報表的過程中,用戶又可以輸人數據庫查詢請求,這又是一個子任務。這樣,操作系統則把每一個請求――工資單報表和新輸人的數據查詢表示爲數據庫進程中的獨立的線程。線程可以在處理器上獨立調度執行,這樣,在多處理器環境下就允許幾個線程各自在單獨處理器上進行。操作系統提供線程就是爲了方便而有效地實現這種併發性。
一個cpu在任意時刻,只能執行一個線程任務,我們平時一邊瀏覽網頁,一邊聽音樂的場景其實是依賴cpu高速地線程間切換實現地,這也引出了一組容易混淆地概念:併發(Concurrency)和並行(Parallelism)。
它們都可以表示兩個或者多個任務一起執行,但是偏重點有些不同。併發偏重於多個任務交替執行,而多個任務之間有可能還是串行的,而並行是真正意義上的“同時執行”,它依賴於多核cpu而實現。
引入線程的好處:
- 易於調度。
- 提高併發性。通過線程可方便有效地實現併發性。進程可創建多個線程來執行同一程序的不同部分。
- 開銷少。創建線程比創建進程要快,所需開銷很少。
- 利於充分發揮多處理器的功能。通過創建多線程進程(即一個進程可具有兩個或更多個線程),每個線程在一個處理器上運行,從而實現應用程序的併發性,使每個處理器都得到充分運行。
進程和線程的關係:
- 一個線程只能屬於一個進程,而一個進程可以有多個線程,但至少有一個線程。線程是操作系統可識別的最小執行和調度單位。
- 資源分配給進程,同一進程的所有線程共享該進程的所有資源。 同一進程中的多個線程共享代碼段(代碼和常量),數據段(全局變量和靜態變量),擴展段(堆存儲)。但是每個線程擁有自己的棧段,棧段又叫運行時段,用來存放所有局部變量和臨時變量。
- 處理機分給線程,即真正在處理機上運行的是線程。
- 線程在執行過程中,需要協作同步。不同進程的線程間要利用消息通信的辦法實現同步。
1.1 線程生命週期
線程是一個動態執行的過程,它也有一個從產生到死亡的過程。
下圖顯示了一個線程完整的生命週期:
- 新建狀態:New,使用 new 關鍵字和 Thread 類或其子類建立一個線程對象後,該線程對象就處於新建狀態。它保持這個狀態直到程序 start() 這個線程。
- 就緒狀態:Runnable,當線程對象調用了start()方法之後,該線程就進入就緒狀態。就緒狀態的線程處於就緒隊列中,要等待JVM裏線程調度器的調度。
- 運行狀態:Running,如果就緒狀態的線程獲取 CPU 資源,就可以執行 run(),此時線程便處於運行狀態。處於運行狀態的線程最爲複雜,它可以變爲阻塞狀態、就緒狀態和死亡狀態。
- 阻塞狀態:Blocked,如果一個線程執行了sleep(睡眠)、suspend(掛起)等方法,失去所佔用資源之後,該線程就從運行狀態進入阻塞狀態。在睡眠時間已到或獲得設備資源後可以重新進入就緒狀態。可以分爲三種:
- 等待阻塞:運行狀態中的線程執行 wait() 方法,使線程進入到等待阻塞狀態。
- 同步阻塞:線程在獲取 synchronized 同步鎖失敗(因爲同步鎖被其他線程佔用)。
- 其他阻塞:通過調用線程的 sleep() 或 join() 發出了 I/O 請求時,線程就會進入到阻塞狀態。當sleep() 狀態超時,join() 等待線程終止或超時,或者 I/O 處理完畢,線程重新轉入就緒狀態。
- 死亡狀態: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 創建線程,前面兩種都是無返回值,而這種方法適合獲取線程執行結果。基本步驟如下:
- 創建 Callable 接口的實現類,並實現 call() 方法,該 call() 方法將作爲線程執行體,並且有返回值。
- 創建 Callable 實現類的實例,使用 FutureTask 類來包裝 Callable 對象,該 FutureTask 對象封裝了該 Callable 對象的 call() 方法的返回值。
- 使用 FutureTask 對象作爲 Thread 對象的 target 創建並啓動新線程。
- 調用 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() | 將當前線程的堆棧跟蹤打印至標準錯誤流。 |
一些重要方法的使用實例待完善…