織夢手機(jī)網(wǎng)站模板上海百度推廣官網(wǎng)
文章目錄
- 前言
- 1. API
- 2. 創(chuàng)建線程
- 2.1. 繼承 Thread類
- 2.2. 實(shí)現(xiàn) Runnable 接口
- 2.3. 匿名內(nèi)部類
- 2.4. lambda
- 2.5.其他方法
- 3. Thread類及其常見的方法和屬性
- 3.1. Thread 的常見構(gòu)造方法
- 3.2. Thread 的常見屬性
- 3.3. start() --- 啟動一個(gè)線程
- 3.4. 中斷一個(gè)線程
- 3.5. 等待線程
- 3.6. 休眠當(dāng)前線程
前言
上一節(jié)課,我們主要引出了線程,以及線程和進(jìn)程的聯(lián)系與區(qū)別,從這篇博客開始,我們開始編寫代碼了,Let’s go!
1. API
線程是操作系統(tǒng)提供的概念,操作系統(tǒng)系統(tǒng)提供一些API供程序員使用。什么是 API 呢?
API (Aoolication Programming Interface) 應(yīng)用程序編程接口,簡單來說,就是別人寫了一些類 / 函數(shù),可以直接拿過來使用。
來源:api 的來源很廣泛,例如 標(biāo)準(zhǔn)庫,第三方庫,其他的各種開源項(xiàng)目,甚至是在工作中,隔壁項(xiàng)目給你提供的代碼。
并且呀,操作系統(tǒng)提供的原生線程 api 是 C語言的,并且不同的操作系統(tǒng)的線程 api 是不一樣的。
為此, Java 對上述內(nèi)容統(tǒng)一封裝,提供Thread (標(biāo)準(zhǔn)庫中的類)
2. 創(chuàng)建線程
2.1. 繼承 Thread類
class MyThread extends Thread{@Overridepublic void run() {while (true){try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("Hello thread!");}}
}
public class Demo1 {public static void main(String[] args) throws InterruptedException {// 方法一:創(chuàng)建一個(gè) MyThread 類繼承 Thread,重寫 run 方法Thread thread = new MyThread();thread.start();while (true){System.out.println("Hello main");Thread.sleep(1000);}}
}
上面便是創(chuàng)建一個(gè) MyThread 類 繼承 Thread,重寫run方法的形式,下面我們來分別說一下。
class MyThread extends Thread{@Overridepublic void run() {while (true){try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("Hello thread!");}}
}
這段代碼中,是我們自己利用JVM自帶的Thread類實(shí)現(xiàn)的一個(gè)MyThread,并且重寫了run方法,當(dāng)我們調(diào)用線程的時(shí)候,就直接運(yùn)行run方法的內(nèi)容了,無需調(diào)用。
為什么不需要調(diào)用 run()方法呢?
因?yàn)?run() 相當(dāng)于 回調(diào)函數(shù)。
例如在Java的數(shù)據(jù)結(jié)構(gòu)中,優(yōu)先級隊(duì)列執(zhí)行比較規(guī)則,如果使用 Comparator 接口,方式就是 compare,如果使用 Comparable接口,方法就是 compareTo,我們沒有調(diào)用這個(gè)函數(shù),他就自己就調(diào)用了,這就是回調(diào)函數(shù)
Thread.sleep(1000);
這個(gè)方法是Thread 的靜態(tài)方法,參數(shù)是毫秒,效果就是,當(dāng)前的線程放棄 CPU 資源,休息一會,時(shí)間過了,再執(zhí)行。為什么要進(jìn)行這個(gè)操作呢,因?yàn)槿绻恍菝叩脑?#xff0c;那么 這兩個(gè)線程會一直吃 CPU 的資源,可能會造成 電腦的卡頓,并且讓 線程休息一下,也便于我們觀察現(xiàn)象。
這個(gè)方法會拋出 InterruptedException 異常,需要我們 使用 try catch 進(jìn)行捕獲。因?yàn)?run()方法沒有 throws ,所以就不能向上拋出異常,只能使用 try catch,這個(gè)跟下面的main方法中的不太一樣。
Thread thread = new MyThread();
這個(gè)就創(chuàng)建了一個(gè)線程 thread,和main 線程一起執(zhí)行。
thread.start();
thread 線程 開始執(zhí)行,此處為什么不調(diào)用 thread.run()
,而是 thread.start()
,
while (true){System.out.println("Hello main");Thread.sleep(1000);}
這段代碼是main線程中執(zhí)行的邏輯,跟上面 thread 幾乎一模一樣,就是為了觀察效果的。
效果顯而易見,我們發(fā)現(xiàn) Hello main 和 Hello thread!,運(yùn)行的過程中,是輪番打印的,并且沒有規(guī)律。
在這說一下如果調(diào)用的是 thread.run()
而不是 thread.start()
,會出現(xiàn)什么現(xiàn)象。
public class Demo01 {public static void main(String[] args) throws InterruptedException {// 方法一:創(chuàng)建一個(gè) MyThread 類繼承 Thread,重寫 run 方法Thread thread = new MyThread();//thread.start();thread.run();while (true){System.out.println("Hello main");Thread.sleep(1000);}}
}
運(yùn)行情況:
相當(dāng)于thread線程就沒有運(yùn)行,因此不要這樣寫代碼。
2.2. 實(shí)現(xiàn) Runnable 接口
class MyRunnable implements Runnable{@Overridepublic void run() {while (true){try {System.out.println("Hello thread!");Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}
public class Demo02 {public static void main(String[] args) throws InterruptedException {Runnable runnable = new MyRunnable();Thread thread = new Thread(runnable);thread.start();while (true){System.out.println("Hello main!");Thread.sleep(1000);}}
}
Runnable runnable = new MyRunnable();
這個(gè)就是一個(gè)任務(wù),一段要執(zhí)行的邏輯,最終還是要通過 Thread ,才能創(chuàng)建真正的線程,那為什么這樣子寫呢?或者就是這樣子寫有什么好處?
可以做到解耦合!
意思就是,讓要執(zhí)行任務(wù)本身這個(gè)事情,和 線程這個(gè)事可以 關(guān)聯(lián)沒有那么大,從而后續(xù)如果要變更代碼 (比如不通過線程執(zhí)行這個(gè)任務(wù),而是通過其他方法…),那么采用 Runnable 這樣的方案,代碼的修改就會更簡單了。
與此相關(guān)的還有一個(gè)名詞高內(nèi)聚。
例如,寫了一個(gè)項(xiàng)目,有很多代碼,有很多文件,也有很多類,以及很多的邏輯,其中把有關(guān)聯(lián)的各種代碼,放到一起,只要把某個(gè)功能邏輯相關(guān)的東西,都放到一起,那么就稱之為高內(nèi)聚,
反之,如果是某個(gè)功能的代碼,東一塊,西一塊的,那么就是低內(nèi)聚。
因此么,我們在寫代碼的時(shí)候,通常要做到高內(nèi)聚,低耦合。
現(xiàn)象跟上面的情況差不多。
2.3. 匿名內(nèi)部類
利用 Thread 來使用 匿名內(nèi)部類
public class Demo03 {public static void main(String[] args) throws InterruptedException {Thread thread = new Thread(){@Overridepublic void run() {while (true){System.out.println("Hello thread!");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}};thread.start();while (true){System.out.println("Hello main!");Thread.sleep(1000);}}
}
Thread thread = new Thread(){};
這個(gè)代碼做了三個(gè)事情:
- 創(chuàng)建了一個(gè) Thread 的子類,子類叫什么名字?不知道,他是個(gè)匿名!
- { } 里面就可以編寫子類的定義代碼,子類里有哪些屬性,要有哪些方法,重寫父類哪些方法…都可以往里面寫。
- 創(chuàng)建了這個(gè)匿名內(nèi)部類的實(shí)例,并且把實(shí)例的引用賦值給了 t。
同理,下面 Runnable 也是這個(gè)意思。
現(xiàn)象如下圖所示:
利用 Runnable 來實(shí)現(xiàn) 匿名內(nèi)部類
public class Demo04 {public static void main(String[] args) throws InterruptedException {Runnable runnable = new Runnable() {@Overridepublic void run() {while (true){System.out.println("Hello Thread!");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}};Thread thread = new Thread(runnable);thread.start();while (true){System.out.println("Hello main!");Thread.sleep(1000);}}
使用匿名內(nèi)部類的好處就是:
少定義一些類。
一般如果某個(gè)代碼是 “一次性的”,就可以使用匿名內(nèi)部類的寫法。
2.4. lambda
其實(shí) lambda 表達(dá)式的本質(zhì),就是回調(diào)函數(shù),使用 () -> {}
public class Demo05 {public static void main(String[] args) throws InterruptedException {Thread thread = new Thread(()->{while (true){System.out.println("Hello Thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});thread.start();while (true){System.out.println("Hello main");Thread.sleep(1000);}}
}
注意。沒有 Runnable 的形式。
咱們上課主要就是使用 lambda 表達(dá)式的形式來創(chuàng)建線程。
2.5.其他方法
不止上面的四種方法,還有兩種,一個(gè)是線程池,另外一種是實(shí)現(xiàn)Callable接口,等到后面我們再說。
3. Thread類及其常見的方法和屬性
3.1. 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) | 線程可以被用來分組管理,分好的組即為線程組,這個(gè)目前了解即可 |
其中第一個(gè)和第二種方法,都已經(jīng)演示過了,直接演示第四個(gè)吧。
public class Demo06 {public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(()->{while (true){System.out.println("Hello t1");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}},"t1");Thread t2 = new Thread(()->{while (true){System.out.println("Hello t2");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}},"t2");t1.start();t2.start();while (true){System.out.println("Hello main");Thread.sleep(1000);}}
}
此時(shí),我們打開JDK自帶的軟件 jconsole (在JDK的bin目錄下面),
其中,沒有用紅色框起來的都是JVM自帶的線程,跟咱們沒有關(guān)系。
其中t1,t2都是我們定義好的線程名稱,如果沒有定義名字的話,會是什么樣子的呢?
是這個(gè)樣子的。
3.2. Thread 的常見屬性
屬性 | 獲取方法 |
---|---|
ID | getId() |
名稱 | getName() |
狀態(tài) | getState() |
優(yōu)先級 | getPriority() |
是否后臺線程 | isDaemon() |
是否存活 | isAlive() |
是否被中斷 | isInterrupted() |
- ID 是線程的唯一標(biāo)識,不同線程不會重復(fù)
- 名稱是各種調(diào)試工具用到的
- 狀態(tài)表示線程當(dāng)前所處的一個(gè)情況,下面我們會進(jìn)一步說明
- 優(yōu)先級高的線程理論上來說更容易被調(diào)度到
- 關(guān)于后臺線程,需要記住:JVM會在一個(gè)進(jìn)程的所有非后臺線程結(jié)束后,才會結(jié)束運(yùn)行
- 是否存活,一個(gè)簡單的理解,就是run方法是否運(yùn)行結(jié)束了
- 線程中斷問題,下面我們再說。
在這又得解釋一些名詞,并且再加上一些代碼。
第一個(gè)如果獲取到線程的名字。
使用這個(gè)類中的函數(shù) Thread.currentThread().getName()
public class Demo07 {public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(()->{while (true) {System.out.println("hello " + Thread.currentThread().getName());try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});t1.start();while (true){System.out.println("hello main");Thread.sleep(1000);}}
}
第二個(gè)什么是isDaemon()?
表格上寫的是后臺線程,但是如果按照英語翻譯的話是 守護(hù)線程。翻譯成守護(hù)線程,但是要把他理解成后臺線程。怎么理解呢?
具體例子:中秋節(jié)親戚們聚餐吃飯,等到一家子人,全部坐齊,舉杯碰一個(gè)之后,然后開始吃飯,但是么,我下午還有課,我就哐哐地吃飯,我吃飽喝足要去上課了,剩余的親戚都沒有吃完呢,但是我就要出門了。
在上述的過程中,把房間看成程序,把自己一個(gè)人悶頭吃飯看做一個(gè)線程(t1),把剩余的人一起吃完飯,看成另一個(gè)線程(t2),對于t1來說,不管t2是否完成,t1一結(jié)束,那么程序必須關(guān)閉,那么此時(shí)就把t2設(shè)置為后臺線程,也就是說,不管大家吃完了沒有(t2是否結(jié)束),我吃完飯,房間門必須要打開(程序就要結(jié)束了),我就要走了。
public static void main(String[] args) {Thread t1 = new Thread(()->{while (true){System.out.println("全家人一直在吃");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});Thread t2 = new Thread(()->{for (int i = 0;i<3;i++){System.out.println("我也在吃,但是吃完飯就要結(jié)束這個(gè)線程");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});t1.setDaemon(true);t1.start();t2.start();}
結(jié)果:
如果沒有setDemon(true),那會發(fā)生什么呢?
public static void main(String[] args) {Thread t1 = new Thread(()->{while (true){System.out.println("全家人一直在吃");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});Thread t2 = new Thread(()->{for (int i = 0;i<3;i++){System.out.println("我也在吃,但是吃完飯就要結(jié)束這個(gè)線程");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});//t1.setDaemon(true);t1.start();t2.start();}
如果按照第二種的話,意思就是說,我都已經(jīng)吃飽喝足了,我還是不走,線程 t2 還要在程序中等待。
獲得 線程的名字 在上面的代碼已經(jīng)使用過了,在這都不寫了。
3.3. start() — 啟動一個(gè)線程
public static void main(String[] args) {Thread t = new Thread(() -> {System.out.println("hello thread");});t.start();}
這個(gè)是個(gè)很簡單的代碼,大家應(yīng)該都可以看懂,如果我們這樣寫代碼,看看會出現(xiàn)什么情況
public static void main(String[] args) {Thread t = new Thread(() -> {System.out.println("hello thread");});t.start();t.start();}
由此我們可以看出,線程不能重復(fù)start。
通過我們查看 線程 start的。
3.4. 中斷一個(gè)線程
我們使用兩種方式來中斷線程,方案一:使用變量 isFinished,方案二:使用interrupt方法。
方案一:使用變量 isFinished。
public class Demo10 {public static boolean isFinshed = false;public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {while (!isFinshed){System.out.println("Hello Thread!");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});t.start();Thread.sleep(3000);isFinshed = true;}
}
上面代碼意思:t線程每隔1秒,打印一個(gè) “Hello Thread!”,3秒以后,不再打印。
這個(gè)是通過 isFinished 變量來確定的。在這提一個(gè)問題:
public static boolean isFinshed = false;
這個(gè)代碼可不可以寫在main函數(shù)里面?
答案是不行的。
public class Demo10 {//public static boolean isFinshed = false;public static void main(String[] args) throws InterruptedException {boolean isFinshed = false;Thread t = new Thread(() -> {while (!isFinshed){System.out.println("Hello Thread!");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});t.start();Thread.sleep(3000);isFinshed = true;}
}
為什么呢?
在第二種寫法中(isFinished變量在main方法中),觸發(fā)了變量捕獲。
lambda是回調(diào)函數(shù),執(zhí)行時(shí)機(jī),是很久以后才會被執(zhí)行的(操作系統(tǒng)真正創(chuàng)建出線程之后),很有可能,后續(xù)線程都創(chuàng)建好了,當(dāng)前main這里的方法都執(zhí)行完了,對應(yīng)的isFinished 都已經(jīng)銷毀了,為了解決這個(gè)問題,Java的做法是,把被捕獲的變量拷貝一份,拷貝到 lambda im,外面的變量是否銷毀,并不影響 lambda方面里面的變量。
但是還是沒有解決問題,拷貝,意味著,這樣的變量就不適合進(jìn)行修改,修改一方,另外一方不會隨之變化,本質(zhì)上是兩個(gè)變量,這種一邊變化,一邊又不變,可能會給程序員帶來更多的疑惑,那么Java大佬直接壓根就不讓這個(gè)變量進(jìn)行修改,這就是變量捕獲操作。
那么為什么第一種都可以了?
因?yàn)榈谝环N寫法把 isFinished 這個(gè)變量改寫成 成員變量,此時(shí)就不再是 “變量捕獲” 語法,
而是切換成 **“內(nèi)部類訪問外部類的成員”**語法。
lambda 本質(zhì)上是 函數(shù)式接口,相當(dāng)于一個(gè)內(nèi)部類,isFinished 變量本身是外部類的成員,內(nèi)部類本來就能夠訪問外部類的成員。并且 成員變量的生命周期,也是讓 GC (垃圾回收)管理的,在 lambda 里面不擔(dān)心變量生命周期失效的問題,那么就不必拷貝,也就不必限制 final 之類。
下面我們使用 Interrupt 方法來終止線程。
public class Demo11 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {while (!Thread.currentThread().isInterrupted()){System.out.println("Hello Thread!");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("t 線程 結(jié)束");});t.start();Thread.sleep(3000);System.out.println("main 線程 嘗試 阻塞 t 線程");t.interrupt();}
}
結(jié)果:
其中:
Thread.currentThread().isInterrupted()
判斷線程時(shí)候被終止了 (其實(shí)是判斷 Thread 里的 boolean 變量的值)
t.interrput()
主動去進(jìn)行終止的 (修改這個(gè) Boolean 變量的值)
當(dāng)然,除了設(shè)置Boolean 變量 (標(biāo)志位) 之外,還能夠喚醒 像 sleep 這樣的阻塞方法。
如果我們把 throw new RuntimeException(e);
這個(gè)代碼給注釋掉,看看會發(fā)生什么情況。
public class Demo11 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {while (!Thread.currentThread().isInterrupted()){System.out.println("Hello Thread!");try {Thread.sleep(1000);} catch (InterruptedException e) {//throw new RuntimeException(e);break;}}System.out.println("t 線程 結(jié)束");});t.start();Thread.sleep(3000);System.out.println("main 線程 嘗試 阻塞 t 線程");t.interrupt();}
}
結(jié)果:
發(fā)生上面的代碼的原因,是 sleep 搗鬼。
正常來說,調(diào)用 interrput 方法就會修改 isInterrputted 方法內(nèi)部的標(biāo)志位 設(shè)置為 true
由于上述代碼,是把 sleep 給 喚醒了
這種喚醒的情況下,sleep 就會在喚醒 之后,把 isInterruptted 標(biāo)志位 設(shè)置為 false
因此在這樣的情況下,如果繼續(xù)執(zhí)行到循環(huán)的條件判定,就會能夠繼續(xù)執(zhí)行。
3.5. 等待線程
多個(gè)線程之間 是并發(fā)執(zhí)行的,也是隨機(jī)調(diào)度的。
站在 程序員的角度來說,咱們不喜歡隨機(jī)的東西的。
join 能夠要求,多個(gè)線程之間結(jié)束的 先后順序
sleep 可以通過 休眠的時(shí)間,來控制線程結(jié)束的順序的,但是有的時(shí)候,這樣的設(shè)定是并不科學(xué)的。
有的時(shí)候,希望 t 先 結(jié)束,main 就可以緊跟著結(jié)束,此時(shí)通過設(shè)置時(shí)間的方式,不一定靠譜。
public class Demo12 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(()->{for (int i = 0;i<3;i++){System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("t 線程結(jié)束");});t.start();t.join();System.out.println("main 線程結(jié)束");}
}
結(jié)果:
此時(shí)上面代碼的邏輯就是,t每休息1s,打印一次 hello thread,執(zhí)行三次,main一直等待。
join 也提供了帶有時(shí)間參數(shù)的版本。意思就是等待的最多時(shí)間
public class Demo12 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(()->{for (int i = 0;i<5;i++){System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("t 線程結(jié)束");});t.start();t.join(3000);System.out.println("main 線程結(jié)束");}
}
此時(shí),t.joiin(3000)
意思就是:如果在 30000ms之內(nèi),t 線程結(jié)束了,那么此時(shí) join 立即繼續(xù)執(zhí)行 (不會等滿 3000ms),如果超過3000ms,t線程還沒結(jié)束,那么此時(shí) join() 往下執(zhí)行,就不等 t 線程了。
這種帶有超時(shí)時(shí)間版本的等待,才是是更加科學(xué)的做法。計(jì)算機(jī)中,尤其是和網(wǎng)絡(luò)通信相關(guān)的邏輯,一般都是需要超時(shí)時(shí)間的#
3.6. 休眠當(dāng)前線程
這個(gè)sleep方法我們之前一直在用,在這我們就不展開細(xì)講了
下節(jié)課,我們就要講述線程的狀態(tài)和線程不安全的情況了,我們不見不散。