??前言👀~
上一章我們介紹了什么是進程,對于進程就了解那么多即可,我們作為java程序員更關注線程,線程內容比較多,所以我們要分好幾部分才能講完
目錄
進程的缺點
?多線程(重要)
進程和線程的區別(經典面試題)
java如何進行多線程編程?
創建線程的方式(面試題)
繼承Thread類(來自java.lang包下)
使用匿名內部類繼承Thread類重寫run
實現Runnable接口
使用匿名內部類實現Runnable接口重寫run
使用lambda表達式創建線程(推薦)
Thread 類及常見方法
Thread類的構造方法
Thread類的常見屬性
啟動線程
中斷線程
等待線程
查看線程狀態
如果各位對文章的內容感興趣的話,請點點小贊,關注一手不迷路,如果內容有什么問題的話,歡迎各位評論糾正 🤞🤞🤞
個人主頁:N_0050-CSDN博客
相關專欄:java SE_N_0050的博客-CSDN博客??java數據結構_N_0050的博客-CSDN博客? java EE_N_0050的博客-CSDN博客
進程的缺點
多進程編程的缺點:進程太重量效率不高,創建進程和銷毀進程和調度進程消耗的時間都是比較多的(消耗在申請資源上),因為我們知道進程是系統資源分配的基本單元,所以在給進程分配資源的時候是一個大活。拿分配內存說,操作系統內部也有一定的數據結構,用來管理空閑的內存,當進程申請內存空間的時候,操作系統就會從這個數據結構中找到大小合適空閑的內存返回給進程。這里的數據結構可以提高一定的效率,但是總體來說和線程相比還是比較耗時的。同時頻繁創建和銷毀進程和進程切換是一個開銷很大的操作,多進程和多線程都能實現并發編程,線程比進程更輕量。有些任務場景需要 “等待 IO”, 為了讓等待 IO 的時間能夠去做?些其他的工作, 也需要用到并發編程. 其次,雖然多進程也能實現 并發編程, 但是線程比進程更輕量
?多線程(重要)
進程想要執行任務就需要依賴線程。線程不能獨立存在,需要依附于進程(進程包含線程,進程可包含一個線程也可包含多個線程),一個進程在最開始的時候,至少要有一個線程(主線程),這個線程負責完成執行代碼的工作,我們也可以根據需要創建多個線程,從而實現"并發編程"的效果換句話說,就是進程中的最小執行單位就是線程
線程也稱輕量級線程(創建、銷毀、調度都比進程快),每個線程就是一個獨立的 "執行流"(因為在執行用戶寫的代碼)可以獨立的執行一些代碼,每一個線程可以執行一系列的操作(也就是代碼)
更好的理解線程,還是拿之前在進程舉的演員的例子,一個舞臺可以有多個演員,但是呢這個多個演員來自不同的劇組,這里面的演員就是線程,劇組就是進程。在我們之前談的進程調度都是基于一個進程只有一個線程,可以理解為之前每個劇組都只有一個演員。實際上,一個進程可以有多個線程,每個線程都可以獨立進行調度。之后談到進程調度的話,不是調度整個進程,而是調度進程中的每一個線程。就比如說這個導演叫這個劇組的所有人來拍戲,所有的演員由導演進行分配角色、上場時間、臺詞等,相當于線程也有狀態、優先級、上下文、記賬信息
下面有圖更好理解多線程
?
一個工廠代表一個進程,一個生產線代表一個線程,我們之前提到的多進程是下面這樣子的?
下面這樣是我們說的多線程,一個進程中有多個線程,同個工廠(共用資源)兩個生產線(多線程)提高了效率
下面這么多人吃100個坤,適當的線程數目(里面的人)能提高效率,但是線程數目過多的情況,效率會降低并且造成線程沖突(線程不安全)
當人數過多且有人吃不到坤的時候,生氣了把雞全扔了,此時引發線程異常,如果我們沒有處理好,可能會導致整個進程崩了,其他線程也會隨之消失
總結:一個進程使用PCB來表示,一個進程可以使用一個PCB表示也可與使用多個PCB表示,每個TCB對應一個線程(可以理解PCB中包含TCB),并且每個線程都有這些信息(狀態、優先級、上下文、記賬信息,但每個線程有自己的程序計數器、虛擬機棧和本地方法棧)輔助調度,除此之外,前面說到的屬性pid是相同的,內存指針、文件描述符表也是共用一份的,根據這些信息我們可以得出線程的特點:每個線程都可以獨立去cpu上調度執行、同一個線程的多個進程之間共用一份內存空間和文件資源,所以我們創建線程的時候不需要像進程一樣申請資源(但是呢創建第一個線程的時候,相當于和進程一起創建的,所以我們要去申請資源,這里申請資源算在進程頭上。后續再創建線程的話就是共享同一份了),我們直接用系統給進程分配好的資源,這樣大大提高了我們的效率和節省開銷。綜上所述我們可以得出進程是資源分配的基本單位,線程是CPU 調度執行的基本單位
每個線程都是獨立調度的,在調度的過程中,系統就不考慮 進程 這樣的概念了。所以就是不同進程中的線程可以被輪番調度,每個線程只有獲得 CPU 的使用權才能執行指令。所謂多線程的并發運行,其實是指從宏觀上看,各個線程輪流獲得 CPU 的使用權,分別執行各自的任務。
進程和線程的區別(經典面試題)
1.線程比進程更輕量、高效,線程不需要申請資源,和同一進程共用一份,省去了申請資源的開銷
2.同一個進程內線程和線程之間會有影響(線程不安全和線程異常),一個線程崩了可能導致其他線程受到影響最終導致進程崩了。進程和進程之間具有獨立性
3.線程依附于進程,一個進程至少有一個線程(主線程),也可以有多個線程
4.線程是調度執行的基本單位,進程是資源分配的基本單位
java如何進行多線程編程?
線程是操作系統的概念,操作系統提供了一套API來操作線程,java對操作系統提供的API進行封裝(跨平臺),我們學java的只需要掌握java封裝過后的這套API就可以操作線程了。
進程和進程之間能并發執行實現并發編程,線程和線程之間也能實現并發執行實現并發編程,我們學java的更側重線程之間的并發執行
創建線程的方式(面試題)
繼承Thread類(來自java.lang包下)
創建Thread對象,我們就可以操作 操作系統內部的線程了。以及重寫入口方法run(描述了該線程要執行的任務)
class MyThread extends Thread {@Overridepublic void run() {System.out.println("我開始工作了");System.out.println("我結束工作了");}
}public class test1 {public static void main(String[] args) {Thread thread = new MyThread();thread.start();System.out.println("我是主線程");}
}
輸出:不確定
再來看下面這段代碼,猜一下輸出順序
class MyThread extends Thread {@Overridepublic void run() {while (true) {System.out.println("thread線程正在工作");}}
}public class test1 {public static void main(String[] args) {Thread thread = new MyThread();thread.start();while (true) {System.out.println("主線程正在工作");}}
}
輸出:交替輸出
為什么呢?我們也不知道這兩個線程是同時執行的還是交替執行的(同一個核心上執行,還是分別在兩個核心上執行),所以我們統稱并發(并行+并發),實現并發編程的效果,為什么要實現并發編程?(充分利用多核cpu的資源)。
注意:打印順序不一定,操作系系統對于多個線程的調度順序是不確定的,隨機的。雖然有先后順序但是誰先誰后我們是不確定的(重要),這個隨機取決于操作系統對于線程調度的模塊(調度器)的具體實現
接著再來看下面這段代碼,猜一下輸出順序
class MyThread extends Thread {@Overridepublic void run() {while (true) {System.out.println("thread線程正在工作");}}
}public class test1 {public static void main(String[] args) {Thread thread = new MyThread();thread.run();while (true) {System.out.println("主線程正在工作");}}
}
輸出:thread線程正在工作? ?不只一條哈
原因:T.run 這種時候只有一個主線程,因為我們沒有創建一個新的進程。等run這個方法結束后,才會執行后面的代碼,相當于只有一個執行流,只能依次執行循環。相當于就是main線程在執行它的run方法,就跟我們在main方法中平常創建一個類然后調用方法一樣,main線程在工作
不信的話代碼拿去自己試試,然后用jdk中自帶的工具jconsole,里面其他線程是JVM創建的
使用匿名內部類繼承Thread類重寫run
同樣創建Thread對象,我們就可以操作 操作系統內部的線程了。以及重寫入口方法run(描述了該線程要執行的任務)
public class test1 {public static void main(String[] args) {Thread thread = new Thread() {@Overridepublic void run() {System.out.println("我是匿名內部類");}};thread.start();System.out.println("我是主線程");}
}
實現Runnable接口
實現Runnable接口,它就表示的是一個可以運行的任務,所以還是需要創建線程來完成這個任務。這樣理解我寫了個任務,丟給線程去完成,但是我們要先把線程創建出來才能去完成
class MyRunnable implements Runnable {@Overridepublic void run() {while (true) {System.out.println("我是Runnable接口");}}
}public class test1 {public static void main(String[] args) {MyRunnable myRunnable = new MyRunnable();Thread thread = new Thread(myRunnable);thread.start();while (true) {System.out.println("主線程正在工作");}}
}
使用Runnable接口和繼承Thread類的區別:主要是為了解耦合,使用Runnable接口相當于跟線程拆分開,把任務抽離出來,可以把這個任務丟給任意一個線程去完成,可以用在需要重復完成這個任務的場景,直接繼承Thread類就不行,它更適合完成一次性的任務
還有關于創建一個線程的時候,有兩個關鍵的操作。一個是明確線程要執行的任務,我們更關注任務本身,如果這個任務就只是執行一段簡單的代碼至于用什么方式實現這個任務沒什么區別。如果遇到復雜的任務有些方式可能就完成不了,這時候我們需要用其他的方式去完成,這時候我們把任務提取出來,我們可以自己選擇指定的方式去完成這個任務?? ?。另外一個操作就是通過調用系統API創建出線程。
使用匿名內部類實現Runnable接口重寫run
public class test1 {public static void main(String[] args) {Thread thread = new Thread(new Runnable() {@Overridepublic void run() {System.out.println("我是匿名內部類");}}) {};thread.start();System.out.println("我是主線程");}
}
使用lambda表達式創建線程(推薦)
public class test1 {public static void main(String[] args) {Thread thread = new Thread(() -> {System.out.println("我是lambda表達式");});thread.start();System.out.println("我是主線程");}
}
除了上面的方式之外,還有其他的方式,后續講解,還有一個點需要清楚就是創建Thread類的時候并沒有真正創建線程,只有在調用start方法的時候調用系統API去創建線程的時候才算認識
Thread 類及常見方法
Thread類的構造方法
在Thread類源碼里有個構造方法可以設置線程的名字,其他的就沒什么好介紹的了
public class test1 {public static void main(String[] args) {Thread thread = new Thread(() -> {while (true) {System.out.println("我是lambda表達式");}}, "我叫線程A");thread.start();}
}
為什么這里沒有顯示main線程呢?因為main線程執行完了,線程的入口方法執行完了,這個線程自然就銷毀了。對于主線程來說,入口方法就是main方法,它調用系統API去創建完線程之后就執行完了。所以如果線程都執行完了,進程也就結束了但只要有一個線程還在執行,進程就不會結束
?
Thread類的常見屬性
1.ID:線程的身份標識就是用來區分線程的,類似進程的pid,只不過這里的ID是java提供的,不是系統api提供的
2.getState()方法:獲取線程狀態,后面有講到
3.isDaemon()方法:判斷是否為后臺線程(守護線程),守護線程就是用來告訴JVM,我的這個線程不重要不需要等待它運行完才退出,讓JVM喜歡什么時候退出就退出。前臺線程(非守護線程)就是告訴JVM,這個線程沒執行完成之前,你不能退出。默認情況一個線程是前臺線程守護進程(后臺進程),默認情況一個線程是前臺線程,我們可以通過setDameon()方法去設置,這么設置之后主線程執行完后,沒有其他前臺線程了,這個進程自然也就結束了
public class test1 {public static void main(String[] args) {Thread thread = new Thread(() -> {while (true) {System.out.println("我是lambda表達式");}}, "我叫線程A");thread.setDaemon(true);//設置thread線程為守護線程thread.start();}
}
4.isAlive()方法:Thread對象的生命周期要比系統內核中的線程更長,線程沒了Thread對象還在,我們要以系統內核中的線程為主。所以我們使用isAlive()進行判斷,判斷系統內核中的線程有沒有結束,結束返回false,沒結束返回true。簡單的理解為 run 方法是否運行結束了
public class test1 {public static void main(String[] args) throws InterruptedException {Thread thread = new Thread(() -> {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("我是lambda表達式");}, "我叫線程A");System.out.println(thread.isAlive());thread.start();System.out.println(thread.isAlive());Thread.sleep(2000);System.out.println(thread.isAlive());}
}
輸出:false true 我是lambda表達式 false
5.isInterrupted()方法:用來判斷線程是否被中斷,怎么判斷呢?Thread內部有一個標志位進行判斷。下面會講到
啟動線程
使用start()方法創建線程
start方法和run方法的區別:非常直白的說,你可以把run看作是任務,start是叫人過來完成這個任務的。專業點說就是start方法通過調用系統API在系統內核中創建線程然后執行run方法中的代碼。run方法會在線程創建好后自動被被調用
中斷線程
中斷一個線程(終止/打斷),讓一個線程停止運行(銷毀),在java中銷毀/終止一個線程做法比較唯一(這個唯一不是說只有一個方法,而是說銷毀一個進程是讓run方法快點執行完),讓run方法快點執行完。在C++中是可以直接強制終止一個線程的運行,就比如你打開一個文本編輯器輸入一段信息,輸入一半直接給你干沒了
先補充兩個方法currentThread()方法和sleep()方法后面要用到
獲取當前線程引用:currentThread()方法返回當前線程對象的引用,哪個線程調用這個方法就獲取哪個線程對象的引用
休眠當前線程:sleep()方法讓線程睡覺的,你可以設置睡多久,然后時間到了系統把它叫醒(阻塞->就緒),注意睡醒之后不會馬上回到cpu上運行,要排隊,這里會涉及調度所以會有一定的開銷。就是你睡醒了需要一點時間緩緩才能去工作。
interrupt()方法:使用interrupt()方法設置標志位,前面說了Thread內部有一個標志位用來判斷線程是否結束,調用這個方法就把這個標志位設置成true。這樣即使我們還在執行sleep方法,它也會被強制喚醒。
來猜猜下面執行代碼線程會是什么狀態以及輸出什么
public class test1 {public static void main(String[] args) throws InterruptedException {Thread thread = new Thread(() -> {while (!Thread.currentThread().isInterrupted()) {System.out.println("hello");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});thread.start();Thread.sleep(3000);thread.interrupt();}
}
答案是輸出幾個hello后報異常,然后說說過程,這個線程先執行sleep方法,然后這個線程處于睡眠,然后你使用了interrupt()方法設置把標志位為true,把這個線程喚醒了,那么這個線程就會繼續工作,除非你不讓它睡眠并且設置標志位為true。舉個例子本來你在上班,然后突然有點困了,你同事讓你睡會,結果領導來了你同事趕緊把你叫醒,你立馬起來了。
使用interrupt()方法搭配sleep方法這樣設置的標志位就像沒效果一樣,沒有把你的線程中斷,為什么這樣設定呢?java期望線程收到中斷的信息的時候,我們能夠自己決定接下來要怎么處理,這樣可以讓我們在開發中有更多的操作空間,前提是通過異常的方式去喚醒。比如你打游戲女朋友叫你陪她去逛街,你可以直接關掉游戲陪她去,也可以說等我打完這把,還可以當個聾子啥也沒聽見。
public class test1 {public static void main(String[] args) throws InterruptedException {Thread thread = new Thread(() -> {while (!Thread.currentThread().isInterrupted()) {System.out.println("hello");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();
// 第一種 System.out.println("一直工作");
//
// 第二種 System.out.println("工作一會休息了");
// break;
//
// 第三種 break;}}});thread.start();Thread.sleep(3000);thread.interrupt();}
}
等待線程
使用join()方法,讓一個線程等待另外一個線程執行結束再執行。前面說線程并發執行的時候,執行的順序是不確定的隨機的,此時我們可以通過這個方法來控制線程結束的順序。并且我們可以設置等待時間,如果沒有設置等待時間,默認的話這個線程會一直等到那個線程執行結束后才會執行自己的任務,類似舔狗有一直舔的也有舔一段時間不舔了
join方法的工作過程:
1.如果主線程正在運行,主線程中調用了A.join方法,此時主線程進入阻塞狀態,A線程執行完,主線程才會解除完成接下來的任務
public class test1 {public static void main(String[] args) throws InterruptedException {Thread thread = new Thread(() -> {for (int i = 0; i < 5; i++) {System.out.println("我先干完活,你再干");}});thread.start();System.out.println("我要干活了");thread.join();System.out.println("輪到我干活了");}
}
輸出結果
2.如果主線程任務已經執行完了,再調用A.join方法,就不用進入阻塞狀態了,直接結束了
public class test1 {public static void main(String[] args) throws InterruptedException {Thread thread = new Thread(() -> {for (int i = 0; i < 5; i++) {System.out.println("我先干完活,你再干");}});thread.start();System.out.println("我馬上干完不給你機會干");thread.join();}
}
輸出結果
查看線程狀態
和進程一樣,線程也有運行、就緒、阻塞狀態。真正在系統調度的還是線程,線程是調度執行的基本單位
在java中,給線程賦予了一些其他的狀態:
1.NEW:Thread對象已經存在了,但是系統線程還沒創建,也就是還沒調用start方法
2.RUNNABLE:就緒狀態,這里有兩種表示,一種是在cpu上執行了,另一種是正等著去cpu上執行
3.TIMED_WATING:阻塞,被sleep這種固定時間的方式打斷產生的阻塞
4.WATING:阻塞,被wait這種不固定時間的方式打斷產生的阻塞
5.BLOCKED:阻塞,等待鎖導致的阻塞,后續死鎖講解
6.TERMINATED:Thread對象還在,操作系統內核的線程結束了,也這樣理解意味著該線程已經完成了其run方法的執行
public class test1 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {while (!Thread.currentThread().isInterrupted()) {System.out.println("java");try {Thread.sleep(2000);} catch (InterruptedException e) {break;}}});System.out.println(t.getState());//只創建了Thread對象的NEW狀態t.start();System.out.println(t.getState());//創建完線程t就是這個RUNNABLE狀態Thread.sleep(1000);System.out.println(t.getState());//t線程處于睡眠t.interrupt();Thread.sleep(1000);System.out.println(t.getState());//t線程執行完了}
}
輸出結果
后續線程出現卡死的情況,我們可以通過上述后面三種狀態去確定卡死的原因是什么,最后兩個狀態后續再講解
上述方法是多線程中非常常用的方法,要好好掌握,今天的內容就先到這,后面我們接著講解線程還有不少的知識點等著💕