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