專欄:JavaEE初階起飛計劃
個人主頁:手握風云
目錄
一、認識線程
1.1. 概念
1.2. 為什么要使用線程
1.3. 進程和線程的關系
1.4. 多線程模型
二、多線程的創建
2.1. 繼承Thread類
2.2. 實現Runnable接口
2.3. 匿名內部類
2.4. lambda表達式
一、認識線程
1.1. 概念
????????進程是操作系統進行資源分配和調度的基本單位。簡單來說,進程就是程序的一次執行過程。通過進程,用戶可以同時運行多個應用程序,提高計算機的利用率和用戶體驗。同時,進程間的隔離性也增強了系統的穩定性和安全性,一個進程的崩潰通常不會影響到其他進程。
1.2. 為什么要使用線程
? ? ? ? 進程雖然說可以實現并發編程的效果,比如寫一個服務器來同時處理多個進程,可以進程級別太高,創建或者銷毀進程的開銷很大。為了優化這些問題,于是引入了線程。
1.3. 進程和線程的關系
? ? ? ? 進程包含線程,線程也可以被稱為輕量級進程。每個線程都可以獨立執行一段邏輯,并且獨立在CPU上調度;同一個進程中的多個線程,共享進程的資源,如內存資源、文件描述符表等。當進程已經有了,在進程內部在創建新線程,就把申請資源開銷省下來了。
1.4. 多線程模型
????????我們假設有一個滑稽老鐵去吃100只燒雞,花費的時間肯定會太長。按照多進程模型,讓兩個滑稽老鐵一人分別吃50只,雖然效率提升了,但我們需要額外申請內存空間。
? ? ? ? 但我們如果按照多線程的模型來,讓兩個滑稽老鐵在同一個桌子上來分別吃50只燒雞,省下了申請資源的開銷,效率就會進一步提高。
? ? ? ? 但線程并不是越多,效率就越高。當滑稽老鐵的數目進一步增多,但桌子就這么大,有可能其中一人吃得好好的,會被其他人擠走。此時非但不會提高效率,效率反而越低了,那此時就只能對機器進行性能優化。
? ? ? ? 當線程數目比較少時,也會出現一些問題:
- 當兩個線程嘗試操作一個共享的資源時,比如內存中的同一個變量,就可能會發生沖突,從而引起bug。
- 如果某個線程拋出異常,且沒有其他代碼把這個異常catch住,就會導致進程內的所有線程會被隨之帶走,整個進程也會結束。
二、多線程的創建
????????多線程編程和多進程編程都是操作系統提供的API,因為Java是一次編譯到處運行的,JVM已經把系統差異給屏蔽了,JVM已經把多線程的API進行了較好地封裝,但多進程的API封裝得比較粗糙。下面就通過代碼來感受一下Java中如何使用多線程。
2.1. 繼承Thread類
public class Demo1 {
// 定義一個靜態內部類MyThread,繼承自Thread類static class MyThread extends Thread {// 重寫run方法@Overridepublic void run() {System.out.println("hello thread");}}public static void main(String[] args) {// 創建一個MyThread對象Thread thread = new MyThread();// 啟動線程thread.start();}
}
????????Thread類是Java中的核心類,用于表示和管理線程。由于操作系統本身就提供了一組操作線程的函數,但這個函數是C語言版本的。JVM把這些操作系統的函數給封裝成了Java版本,到了程序員手中,就成了Thread。使用Thread類,就相當于在使用操作系統的API。run方法是線程執行體,包含了線程需要執行的任務代碼。當線程啟動時,run方法中的代碼將在新的線程中執行,也就是多線程程序的入口。
? ? ? ? 在MyThread類里面重寫的是run()方法,但在main()方法里調用的卻是start()方法,這是因為start()方法是調用系統API,真正在操作系統內部創建一個線程,這個新的線程就會以run()作為入口,執行里面的邏輯,而run()方法就不需要在代碼中顯式調用。
? ? ? ? 但如果我們調用run()方法,雖然也能打印"hello thread",但沒有創建新的線程,而是直接在main()方法所在的 “主線程” 里執行了run()方法的邏輯。對于一個進程,至少得有一個線程;對于一個Java程序來說,main()方法,所在的進程至少會有這個線程,這個線程就是主線程。上面的代碼就具有一個主線程和一個新創建的線程。
- sleep靜態方法
? ? ? ? Thread.sleep是Thread類中的一個靜態方法。當調用這個方法時,當前線程會被阻塞,不執行任何操作,直到指定的毫秒數過去。這個方法可以用于多種場景,比如在循環中添加延遲,或者在執行某些操作之前等待一段時間。
public class MyThread extends Thread {@Overridepublic void run() {System.out.println("Hello Thread!");}
}
public class Demo2 {public static void main(String[] args) {// 創建一個MyThread對象Thread thread = new MyThread();// 啟動線程thread.start();while (true) {System.out.println("Hello Main!");// 休眠1000秒Thread.sleep(1000);}}
}
? ? ? ? 但此時的代碼會顯示出錯誤,原因是我們有受查的異常,必須進行顯式處理,可以用throws拋出或者try{}catch{}進行捕獲。
//throws
public class Demo2 {public static void main(String[] args) throws InterruptedException {// 創建一個MyThread對象Thread thread = new MyThread();// 啟動線程thread.start();while (true) {System.out.println("Hello Main!");// 休眠1000秒Thread.sleep(1000);}}
}//try{}catch{}
public class Demo2 {public static void main(String[] args) {// 創建一個MyThread對象Thread thread = new MyThread();// 啟動線程thread.start();while (true) {System.out.println("Hello Main!");// 休眠1000秒try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}
? ? ? ? 同理,我們在MyTread類里面進行循環打印。但這里只能使用try{}catch{}進行捕獲,因為run()方法是重寫自父類的方法,如果使用throws拋出不符合父類方法的聲明。
? ? ? ? 完整代碼實現:
public class MyThread extends Thread {@Overridepublic void run() {while (true) {System.out.println("Hello Thread!");try {// 休眠1秒Thread.sleep(1000);} catch (InterruptedException e) {// 拋出運行時異常throw new RuntimeException(e);}}}
}
public class Demo2 {public static void main(String[] args) throws InterruptedException {// 創建一個MyThread對象Thread thread = new MyThread();// 啟動線程thread.start();while (true) {System.out.println("Hello Main!");// 休眠1000秒Thread.sleep(1000);}}
}
? ? ? ? 運行結果如下,雖然兩個打印分別在不同的while循環中,兩個線程屬于并發關系,獨立在CPU上執行。我們同時也可以看到兩個線程不是嚴格交替執行的,由于兩個線程都加了休眠,當1000毫秒到后,哪個線程線程的執行順序是無法確定的,這是因為操作系統調度的線程是隨機不可預測的。
? ? ? ? 我們還有更直觀的辦法來觀察多線程:在我們安裝的JDK里面的bin目錄下,以管理員身份運行jconsole.exe,然后點擊連接本地進程Demo2,然后點擊線程,就可以看到我們的Thread-0和main兩個線程,剩下的線程都是JVM自帶的,這些線程進行了一些背后的操作,比如垃圾回收、記錄統計信息、記錄一些調試信息等。
2.2. 實現Runnable接口
public class Demo3 {public static void main(String[] args) throws InterruptedException {// 創建一個實現了Runnable接口的實例Runnable myRunnable = new MyRunnable();// Runnable本身沒有start方法,需要配合Thread類來使用// 創建一個Thread對象,并將myRunnable作為參數傳入Thread thread = new Thread(myRunnable);// 啟動線程thread.start();while (true) {System.out.println("Hello Main!");Thread.sleep(1000);}}
}class MyRunnable implements Runnable {@Overridepublic void run() {while (true) {System.out.println("Hello Tread!");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}
}
????????運行結果:
? ? ? ? 對于第一種繼承Thread寫法,描述任務的時候,代碼是寫到Thread子類中的,意味著任務內容與Thread類耦合度高,未來想把這個任務給別的主體來執行。
? ? ? ? 對于第二種Runnable接口寫法,任務是寫到Runnable中,不涉及到任何和“線程”的概念,任務內容和Thread耦合度幾乎沒有,后序可以把這個任務交給進程或者協程來執行。
2.3. 匿名內部類
public class Demo4 {public static void main(String[] args) {// 創建一個新的線程Thread thread = new Thread() {@Overridepublic void run() {while (true) {System.out.println("Hello Tread!");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}};// 啟動線程thread.start();while (true) {// 在主線程中輸出"Hello Main!"System.out.println("Hello Main!");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}
}
? ? ? ? 此處創建了沒有的名字匿名內部類,并且這個類是Thread類的子類,該子類也重寫了run()方法,也創建了子類實例,通過thread引用指向子類。這樣寫可以簡化代碼。
? ? ? ? 運行結果如下:
2.4. lambda表達式
public class Demo5 {public static void main(String[] args) {// 創建一個新的線程Thread thread = new Thread(() -> {while (true) {System.out.println("Hello Thread!");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});// 啟動線程thread.start();// 無限循環while (true) {System.out.println("Hello Main!");try {// 主線程休眠1秒Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}
}
? ? ? ? 運行結果如下: