文章目錄
- 前言
- 我們為什么要使用線程而不是進程來實現并發編程
- 什么是線程
- 進程和線程的區別
- 如何使用Java實現多線程
- 創建線程
- 1.創建一個繼承 Thread 類的線程類
- 2.實現 Runnable 接口
- 匿名內部類方式實現 Runnable 接口
- lambda 表達式實現 Runnable 接口
- Thread 類的常見構造方法
- Thread 的幾個常見屬性
- 啟動線程
- 終止線程
- 1.自定義標志位終止線程
- 2.使用 Thread 自帶的標志位終止線程
- 線程等待
前言
前面我們了解了什么是進程以及如何實現進程調度,那么今天我將為大家分享關于線程相關的知識。在學習線程之前,我們認為進程是操作系統執行獨立執行的單位,但其實并不然。線程是操作系統中能夠獨立執行的最小單元。只有掌握了什么是線程,我們才能實現后面的并發編程。
我們為什么要使用線程而不是進程來實現并發編程
實現并發編程為什么不使用多進程,而是使用多線程呢?主要體現在幾個方面:
- 創建一個進程的開銷很大
- 調度一個進程的開銷很大
- 銷毀一個進程的開銷很大
開銷不僅體現在時間上,還體現在內存和 CPU 上。現在以”快“著稱的互聯網時代,這種大開銷是不受人們歡迎的。那么為什么多線程就可以實現快捷的并發編程呢?
- 共享資源:多個線程之間共用同一部分資源,大大減少了資源的浪費
- 創建、調度、銷毀的開銷小:相較于進程的創建、調度和銷毀,線程的創建、調度和銷毀就顯得很輕量,這樣也大大節省了時間和資源的浪費
- 現在的計算機 CPU 大多都是多核心模式,我們的多線程模式也更能利用這些優勢
什么是線程
線程是操作系統能夠獨立調度和執行的最小執行單元。線程是進程內的一個執行流程,也可以看作是進程的子任務。與進程不同,線程在進程內部創建和管理,并且與同一進程中的其他線程共享相同的地址空間和系統資源。
只有當第一個線程創建的時候會有較大的開銷,后面線程的創建開銷就會小一點。并發編程會盡量保證每一個線程在不同的核心上單獨執行,互不干擾,但也不可避免的出現在單核處理器系統中,線程在一個 CPU 核心上運行,它們通過時間片輪轉調度算法使得多個線程輪流執行,給我們一種同時執行感覺。
線程是操作系統調度執行的基本單位
進程和線程的區別
一個進程中可以包含一個線程,也可以包含多個線程。
-
資源和隔離:進程是操作系統中的一個獨立執行單位,具有獨立的內存空間、文件描述符、打開的文件、網絡連接等系統資源。每個進程都擁有自己的地址空間,進程間的數據不共享。而線程是進程內的執行流程,共享同一進程的地址空間和系統資源,可以直接訪問和修改相同的數據。
-
創建和銷毀開銷:相對于進程,線程的創建和銷毀開銷較小。線程的創建通常只涉及創建一個新的執行上下文和一些少量的內存。而進程的創建需要分配獨立的內存空間、加載可執行文件、建立進程控制塊等操作,開銷較大。
-
并發性和響應性:由于線程共享進程的地址空間,多個線程可以在同一進程內并發執行任務,共享數據和通信更加方便。因此,線程的切換成本較低,可以實現更高的并發性和響應性。而進程之間通常需要進程間通信(IPC)的機制來進行數據交換和共享,開銷較大,響應性較低。
-
管理和調度:進程由操作系統負責管理和調度,每個進程之間是相互獨立的。而線程是在進程內部創建和管理的,線程調度和切換由操作系統的線程調度器負責。線程的調度通常比進程的調度開銷小,線程切換更快。
-
安全性和穩定性:由于進程之間相互獨立,一個進程的崩潰不會影響其他進程的正常運行,因此進程具有更好的安全性和穩定性。而一個線程的錯誤或異常可能會導致整個進程崩潰。
前面我們所說的 PCB 其實也是針對線程來說的,一個線程具有一個 PCB 屬性,一個進程可以含有一個或多個 PCB。
PCB 里的狀態:上下文,優先級,記賬信息,都是每個線程有自己的,各自記錄自己的,但是同一個進程里的PCB之間,pid是一樣的,內存指針和文件描述符表也是一樣的。
如何使用Java實現多線程
在Java中使用一個線程大致分為以下幾個步驟:
- 創建線程
- 啟動線程
- 終止線程
- 線程等待
創建線程
在Java中執行線程操作依賴于 Thread
類。并且創建一個線程具有多種方法。
- 創建一個線程類繼承自 Thread 類
- 實現 Runnable 接口
1.創建一個繼承 Thread 類的線程類
class MyThread extends Thread {@Overridepublic void run() {System.out.println("這是一個MyThread線程");}
}
我們需要重寫 run 方法,而 run 方法是指該線程要干什么。
創建實例對象
public class TreadDemo1 {public static void main(String[] args) {Thread t = new MyThread();}
}
2.實現 Runnable 接口
創建一個線程我們不僅可以直接創建一個繼承自 Thread 的線程類,我們也可以直接實現 Runnable 接口,因為通過源碼我們可以知道 Thread 類也實現了 Runnable 接口。
我們可以將 Runnable 作為一個構造方法的參數傳進去。
class MyRunnable implements Runnable {@Overridepublic void run() {System.out.println("這是一個線程");}
}public class ThreadDemo2 {public static void main(String[] args) {Thread t = new Thread(new MyRunnable());}
}
但是這種實現 Runnable 接口的方式會顯得很麻煩,因為每個線程執行的內容大多是不同的,所以我們可以采用下面兩種方式來實現 Runnable 接口。
- 匿名內部類
- lambda 表達式
匿名內部類方式實現 Runnable 接口
public class ThreadDemo3 {public static void main(String[] args) {Thread t = new Thread(new Runnable() {@Overridepublic void run() {System.out.println("這是一個線程");}});}
}
lambda 表達式實現 Runnable 接口
public class ThreadDemo4 {public static void main(String[] args) {Thread t = new Thread(() -> {System.out.println("這是一個線程");});}
}
Thread 類的常見構造方法
方法 | 說明 |
---|---|
Thread() | 創建對象 |
Thread(Runnable target) | 使用 Runnable 對象創建線程對象 |
Thread(String name) | 創建線程對象,并命名 |
Thread(Runnable target,String name) | 使用 Runnable 對象創建線程對象,并命名 |
Thread(ThreadGroup group,Runnable target | 線程可以被用來分組管理,分好的組即為線程組,這個我們目前了解即可 |
Thread 類有很多構造方法,大家有興趣可以自己去看看。
Thread 的幾個常見屬性
屬性 | 獲取方法 |
---|---|
ID | getId() |
名稱 | getName() |
狀態 | getState() |
優先級 | getPriority() |
是否后臺進程 | isDaemon() |
是否存活 | isAlive |
是否被中斷 | isInterrupted() |
- ID 是線程的唯一標識,不同線程不會重復
- 名稱是各種調試工具用到
- 狀態表示線程當前所處的一個情況
- 優先級高的線程理論上來說更容易被調度到
- 關于后臺線程,需要記住一點:JVM會在一個進程的所有非后臺線程結束后,才會結束運行。
- 是否存活,即簡單的理解,為 run 方法是否運行結束了
- 線程的中斷問題,下面我們進一步說明
我們前面創建的都是前臺進程,我們可以感知到的,那么什么叫做后臺進程呢?
后臺進程是指在計算機系統中以低優先級運行且不與用戶交互的進程。與前臺進程相比,后臺進程在運行時不會占據用戶界面或終端窗口,并且通常在后臺默默地執行任務。
后臺進程通常用于執行系統服務、長時間運行的任務、系統維護或監控等。它們在后臺運行,不需要用戶的直接參與或操作,而且可以持續運行,即使用戶退出或注銷系統。
啟動線程
我們上面只是創建了線程,要想讓線程真正的起作用,我們需要手動啟動線程。線程對象.start()
class MyRunnable implements Runnable {@Overridepublic void run() {System.out.println("這是一個線程");}
}public class ThreadDemo2 {public static void main(String[] args) {Thread t = new Thread(new MyRunnable());t.start();}
}
這里有人看到輸出結果可能會問了,這跟我直接調用 run 方法好像沒什么區別吧?我們這個代碼肯定看不出來區別,所以我們稍稍修改一下代碼。
class MyRunnable implements Runnable {@Overridepublic void run() {while(true) {System.out.println("hello MyThread!");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}public class ThreadDemo2 {public static void main(String[] args) {Thread t = new Thread(new MyRunnable());t.start();while(true) {System.out.println("hello main!");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}
這里 Thread.sleep()
的作用是使線程停止一會,防止進行的太快,我們不容易看到結果,并且這里的 Thread.sleep()
方法還需要我們拋出異常
我們可以看到這里的執行結果是 main 線程和 t 線程都執行了,而不是只執行其中的一個線程。不僅如此,這兩個線程之間沒有什么規定的順序執行,而是隨機的,這種叫做搶占式執行,每個線程都會爭搶資源,所以會導致執行順序的不確定,也正是因為多線程的搶占式執行,會導致后面的線程安全問題。
那么我們再來看看,如果直接調用 run 方法,而不是 start 方法會有什么結果。
當直接調用 run 方法的話,也就只會執行 t 對象的 run 方法,而沒有執行 main 方法后面的代碼,也就是說:當直接調用 run 方法的時候,線程并沒有真正的啟動,只有調用 start 方法,線程才會啟動。
我們也可以通過 Java 自帶的 jconsle
來查看當前有哪些Java進程。
我們需要找到 jconsole.exe
可執行程序。通常在這個目錄下C:\Program Files\Java\jdk1.8.0_192\bin
我們也可以點進來看看。
終止線程
通常當主線程 main 執行完 mian 方法之后或者其他線程執行完 run 方法之后,線程就會終止,但是我們也可以在這之前手動終止線程。但是我們這里終止線程并不是立刻終止,也就相當于這里只是建議他這個線程停止,具體要不要停止得看線程的判斷。
- 自定義標志位來終止線程
- 使用 Thread 自帶的標志位來終止線程
1.自定義標志位終止線程
public class ThreadDemo4 {private static boolean flg = false; //定義一個標志位public static void main(String[] args) {Thread t = new Thread(() -> {while(!flg) {System.out.println("hello mythread!");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});t.start();System.out.println("線程開始");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}flg = true; /修改標志位使線程終止System.out.println("線程結束");}
}
2.使用 Thread 自帶的標志位終止線程
可以使用 線程對象.interrupt()
來申請終止線程。并且使用 Thread.currentThread,isInterrupted()
來判斷是否終止線程。
Thread.currentThread()
獲取到當前線程對象
public class ThreadDemo4 {private static boolean flg = false;public static void main(String[] args) {Thread t = new Thread(() -> {while(!Thread.currentThread().isInterrupted()) {System.out.println("hello mythread!");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});t.start();System.out.println("線程開始");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}t.interrupt();System.out.println("線程結束");}
}
發現了沒,這里拋出了異常,但是線程并沒有終止,為什么呢?問題出在哪里呢?
其實這里問題出在 Thread.sleep
上,如果線程在 sleep 中休眠,此時調用 interrupt() 會終止休眠,并且喚醒該線程,這里會觸發 sleep 內部的異常,所以我們上面的運行結果就拋出了異常。那么為什么線程又被喚醒了呢?
interrupt 會做兩件事:
- 把線程內部的標志位給設置成true,也就是
!Thread.current.isInterrupt()
的結果為true - 如果線程在進行 sleep ,就會觸發吟唱,把 sleep 喚醒
但是 sleep 在喚醒的時候,還會做一件事,把剛才設置的這個標志位,再設置回false(清空標志位),所以就導致了線程繼續執行。那么如何解決呢?
很簡單,因為 sleep 內部發生了異常,并且我們捕獲到了異常,所以我們只需要在 catch
中添加 break
就行了。
try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();break;}
這也就相當于,我 t 線程拒絕了你的終止請求。
線程等待
在多線程中,可以使用 線程對象.join()
來使一個線程等待另一個線程執行完或者等待多長時間后再開始自己的線程。
方法 | 說明 |
---|---|
public void join() | 等待線程結束 |
public void join(long millis) | 等待線程結束,最多等 millis 毫秒 |
public void join(long millis,int nanos) | 同理,但可以更高精度 |
public class ThreadDemo5 {public static void main(String[] args) {Thread t = new Thread(() -> {for(int i = 0; i < 5; i++) {System.out.println("hello mythread!");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});t.start();try {t.join();} catch (InterruptedException e) {throw new RuntimeException(e);}for(int i = 0; i < 5; i++) {System.out.println("hello main!");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}
在那個線程中調用的 線程對象.join()
就是哪個線程等待,而哪個線程調用 join()
方法,那么這個線程就是被等待的。而這個等待的過程也被稱為阻塞。如果在執行 join 的時候,調用 join 方法的線程如果已經結束了,那么就不會發生阻塞。