1. 認識線程(Thread)
1.1 概念
1) 線程是什么
一個線程就是一個 "執行流". 每個線程之間都可以按照順序執行自己的代碼. 多個線程之間 "同時" 執行著多份代碼.
舉例:
還是回到我們之前的銀?的例?中。之前我們主要描述的是個?業務,即?個?完全處理??的業務。我們進?步設想如下場景:?家公司要去銀?辦理業務,既要進?財務轉賬,?要進?福利發放,還得進?繳社保。如果只有張三?個會計就會忙不過來,耗費的時間特別?。為了讓業務更快的辦理好,張三?找來兩位同事李四、王五?起來幫助他,三個?分別負責?個事情,分別申請?個號碼進?排隊,?此就有了三個執?流共同完成任務,但本質上他們都是為了辦理?家公司的業務。 此時,我們就把這種情況稱為多線程,將?個?任務分解成不同?任務,交給不同執?流就分別排隊執?。其中李四、王五都是張三叫來的,所以張三?般被稱為主線程(Main Thread)。
2) 為何要有線程 ?
首先,我們來說一下進程。在多任務操作系統中,希望系統能夠同時運行多個程序,這就引入了進程。如果是單任務的操作系統,就完全不涉及進程,也不需要管理(進程),更不需要調度。本質上說,進程是解決”并發編程“問題的,事實上,進程也可以很好地解決并發編程這樣的問題。
但是在一些特定的環境下,進程的表現不盡人意,比如,有些場景下,需要頻繁的創建和銷毀進程,舉例,最早的web開發,是使用C語言來編寫的服務器程序(基于一種CGI這樣的技術,其基于多進程的編程模式),服務器同一時刻會收到很多請求,針對每個請求,都會創建出一個進程,給這個請求提供一定的服務,返回對應的響應;一旦這個請求處理完了,此時這個進程就要銷毀了。如果請求很多,就意味著服務器要不停地創建進程、銷毀進程,此時使用多進程編程,系統的開銷就會很大(開銷主要體現在資源的申請和釋放上)。?
?先, "并發編程" 成為 "剛需"
- 單核 CPU 的發展遇到了瓶頸. 要想提?算力, 就需要多核 CPU,而并發編程能更充分利?多核 CPU資源
- 有些任務場景需要 "等待 IO", 為了讓等待 IO 的時間能夠去做?些其他的?作, 也需要?到并發編程
- 創建線程?創建進程更快.
- 銷毀線程?銷毀進程更快.
- 調度線程?調度進程更快.
多線程并發編程,效率更高,尤其是對于java進程,是要啟動java虛擬機的,啟動java虛擬機開銷是更大的,搞多個java進程,就要多個java虛擬機。所以,java中不太去鼓勵多進程編程。
3) 進程和線程的區別
- 進程是包含線程的. 每個進程?少有?個線程存在,即主線程。
- 進程和進程之間不共享內存空間. 同?個進程的線程之間共享同?個內存空間.
?如之前的多進程例?中,每個客戶來銀?辦理各?的業務,但他們之間的票據肯定是不想讓別?知道的,否則錢不就被其他?取?了么。?上?我們的公司業務中,張三、李四、王五雖然是不同的執?流,但因為辦理的都是?家公司的業務,所以票據是共享著的。這個就是多線程和多進程的最?區別。
- 進程是系統分配資源的最小單位,線程是系統調度的最小單位。
- ?個進程掛了?般不會影響到其他進程. 但是?個線程掛了, 可能把同進程內的其他線程?起帶?(整個進程崩潰).
另外注意:
- 同一個進程中的線程之間,可能會互相干擾,引起線程安全問題
- 線程也不是越多越好,要能夠合適,如果線程太多了,調度開銷可能會非常明顯?
?
4) Java 的線程 和 操作系統線程 的關系
操作系統內核,是操作系統最核心部分的功能模塊(管理硬件、給軟件提供穩定的運行環境)。操作系統 = 內核 + 配套的應用程序這里用銀行為例來說明一下:當你到銀行進行各種業務的辦理的時候,都是需要在辦事窗口前,給工作人員說清楚你的需求,由工作人員代辦。我們知道銀行中的辦事窗口內部和銀行大廳是分隔開的,你是進不去辦事窗口內部的, 這里的辦事窗口內部就相當于操作系統內核空間(內核態),你所在大廳則是用戶空間(用戶態)。?
為什么劃分出用戶態、內核態:
?最主要的目的,還是為了“穩定”。防止你的應用程序,把硬件設備或軟件資源給搞壞了。系統封裝了一些api,這些api都屬于是一些“合法”的操作,應用程序只能調用這些api,這樣就不至于對系統/硬件設備產生太大的危害。
假設讓應用程序直接操作硬件,可能極端情況下,代碼出現bug,就把硬件干燒了。
?1.2 第?個多線程程序
- 每個線程都是?個獨立的執行流
- 多個線程之間是 "并發" 執行的
class MyThread2 extends Thread {//Thread類不用導包,屬于特殊的包java.long,該包默認自動導入@Overridepublic void run() {//run方法就是該線程的入口方法while (true) {System.out.println("hello run");try {Thread.sleep(1000);}catch (InterruptedException e) {e.printStackTrace();}}}
}
public class ThreadDemo2 {public static void main(String[] args) {//2、根據剛才的類,創建出具體的實例(線程實例,才是真正的線程)Thread t = new MyThread2();//3、調用Thread的start方法,才會真正調用系統api,在系統內核中創建出線程t.start();while (true) {System.out.println("hello main");try {Thread.sleep(1000);}catch (InterruptedException e) {e.printStackTrace();}}}
}
對于上述代碼,運行結果為兩個循環不停地同時輸出(驗證了多個線程之間是 "并發" 執行的)。
我們知道,若對于普通程序來說,當遇到一個無限循環,會停留在這個循環,不停的打印輸出,后續的代碼是執行不到的。然而這個多線程程序,兩個循環都執行到了,是因為每個線程都是?個獨立的執行流 。代碼中 t.start() ,即調用start()之后會創建一個新的線程,該線程進入到 run 方法,進行循環;而此時main線程,這個主線程會繼續自己的執行,執行后續代碼,也進行循環。
這里可以使用 jconsole 命令觀察線程:
2. 創建線程的幾種方法
-
?方法1 繼承 Thread 類
我們上面寫的第一個多線程程序就是用的該方法。
class MyThread extends Thread {@Overridepublic void run() {System.out.println("這?是線程運?的代碼");}
}
MyThread t = new MyThread();
t.start(); //調用start才會真正地創建線程
-
方法2 實現 Runnable 接口
1、實現 Runnable 接口
class MyRunnable implements Runnable {@Overridepublic void run() {System.out.println("這?是線程運?的代碼");}
}
?2、創建 Thread 類實例, 調用 Thread 的構造方法時將 Runnable 對象作為 target 參數.
Thread t = new Thread(new MyRunnable());//或者另一種寫法
Runnable runnable = new MyRunnable();
Thread t = new Thread(runnable);
?3、調用start方法
t.start(); // 線程開始運?
該方法完整代碼示例:
class MyThread3 implements Runnable {@Overridepublic void run() {while (true) {System.out.println("hello runnable");try {Thread.sleep(1000);}catch (InterruptedException e) {e.printStackTrace();}}}
}
public class ThreadDemo3 {public static void main(String[] args) {
// Runnable runnable = new MyThread3();
// Thread t = new Thread(runnable);Thread t = new Thread(new MyThread3());t.start();while (true) {System.out.println("hello main");try {Thread.sleep(1000);}catch (InterruptedException e) {e.printStackTrace();}}}
}
- 繼承 Thread 類, 直接使用 this 就表示當前線程對象的引用.
- 實現 Runnable 接口, this 表示的是 MyRunnable 的引用,需要使用Thread.currentThread()來表示當前線程對象
其他創建方法
-
匿名內部類創建 Thread 子類對象
// 使?匿名類創建 Thread ?類對象
Thread t1 = new Thread() {@Overridepublic void run() {System.out.println("使?匿名類創建 Thread ?類對象");}
};
-
匿名內部類創建 Runnable 子類對象?
// 使?匿名類創建 Runnable ?類對象
Thread t2 = new Thread(new Runnable() {@Overridepublic void run() {System.out.println("使?匿名類創建 Runnable ?類對象");}
});
-
lambda 表達式創建 Runnable 子類對象?
// 使? lambda 表達式創建 Runnable ?類對象
Thread t3 = new Thread(() -> System.out.println("使?匿名類創建 Thread ?類對象"));
Thread t4 = new Thread(() -> {System.out.println("使?匿名類創建 Thread ?類對象");
});
3. 多線程的優勢-增加運行速度
- 使用 System.nanoTime() 可以記錄當前系統的 納秒 級時間戳.
- serial 串行的完成?系列運算. concurrency 使用兩個線程并行的完成同樣的運算.
public class ThreadAdvantage {// 多線程并不?定就能提?速度,可以觀察,count 不同,實際的運?效果也是不同的private static final long count = 10_0000_0000;public static void main(String[] args) throws InterruptedException {// 使?并發?式concurrency();// 使?串??式serial();}private static void concurrency() throws InterruptedException {long begin = System.nanoTime();// 利??個線程計算 a 的值Thread thread = new Thread(new Runnable() {@Overridepublic void run() {int a = 0;for (long i = 0; i < count; i++) {a--;}}});thread.start();// 主線程內計算 b 的值int b = 0;for (long i = 0; i < count; i++) {b--;}// 等待 thread 線程運?結束thread.join();// 統計耗時long end = System.nanoTime();double ms = (end - begin) * 1.0 / 1000 / 1000;System.out.printf("并發: %f 毫秒%n", ms);}private static void serial() {// 全部在主線程內計算 a、b 的值long begin = System.nanoTime();int a = 0;for (long i = 0; i < count; i++) {a--;}int b = 0;for (long i = 0; i < count; i++) {b--;}long end = System.nanoTime();double ms = (end - begin) * 1.0 / 1000 / 1000;System.out.printf("串?: %f 毫秒%n", ms);}
}
?
該篇是對多線程的初步認識,接下來我會繼續更新多線程的相關內容,請多多關注!