面試中最常被虐的地方必定有併發編程這塊知識點,不管你是剛剛入門的大四萌新仍是2-3年經驗的CRUD怪,也就是說這類問題你最起碼會被問3年,何不花時間死磕到底。消除恐懼最好的辦法就是面對他,奧利給!(這一系列是本人學習過程當中的筆記和總結,並提供調試代碼供你們玩耍java
1.java線程生命週期和java線程狀態都有哪些?git
2.java線程生命週期之間是如何轉換的?github
3.Thread.start()都作了哪些事情?面試
請自行回顧以上問題,若是還有疑問的自行回顧上一章哦~編程
本章學習完成,你將會掌握Thread經常使用API接口的使用,包括sleep、yield和join,而且會詳細解析join源碼和用法。同時配合上一章的start()方法,本章還會介紹一下應該如何去關閉一個線程。鑑於interrupt字段內容較多,咱們放到下一章講哦。(老規矩,熟悉這塊的同窗能夠選擇直接關注點贊👍完成本章學習哦!)bash
本章代碼下載併發
本節開頭先打個預防針,針對每個API會用和精通是兩個水準哦,這裏咱們的目標是徹底吃透,因此章節內容會比較幹,可是我會加油寫的有代入感,你們一塊兒加油~👏app
sleep一共有兩個重載方法ide
因爲這兩個實現精度不一樣,內部調用的都是同一個方法,因此咱們這裏就挑public static void sleep(long millis, int nanos) throws InterruptedException
來看下。函數
/**
* Causes the currently executing thread to sleep (temporarily cease
* execution) for the specified number of milliseconds plus the specified
* number of nanoseconds, subject to the precision and accuracy of system
* timers and schedulers. The thread does not lose ownership of any
* monitors.
*
* @param millis
* the length of time to sleep in milliseconds
*
* @param nanos
* {@code 0-999999} additional nanoseconds to sleep
*
* @throws IllegalArgumentException
* if the value of {@code millis} is negative, or the value of
* {@code nanos} is not in the range {@code 0-999999}
*
* @throws InterruptedException
* if any thread has interrupted the current thread. The
* <i>interrupted status</i> of the current thread is
* cleared when this exception is thrown.
*/
public static void sleep(long millis, int nanos)
throws InterruptedException {
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
millis++;
}
sleep(millis);
}
複製代碼
官方的描述是這樣的,使線程暫時中止執行,在指定的毫秒數上再加上指定的納秒數,可是線程不會失去監視器
。這裏的關鍵是不會失去持有的監視器,上一章咱們講過這時線程處於BLOCKED
階段。
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
複製代碼
當millis<0或者nanos不在0-999999範圍中的時候就會拋出IllegalArgumentException
@throws InterruptedException
* if any thread has interrupted the current thread. The
* <i>interrupted status</i> of the current thread is
* cleared when this exception is thrown.
複製代碼
sleep
可被中斷方法打斷,可是會拋出InterruptedException
異常。
好啦到這裏咱們介紹完了這個API了,是否是感受很簡單呢?哈哈光這樣可不行,實踐是檢驗真理的惟一標準下面咱們來驗證一下sleep
以後對象監視鎖到底有沒有釋放。
別犯困啦,劃重點啦
/**
* 建立一個獨佔鎖
*/
private static final Lock lock = new ReentrantLock();
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
lock.lock();
System.out.println("我是" + Thread.currentThread().getName() + ",lock在我手中");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
System.out.println(Thread.currentThread().getName() + "不須要lock了");
}
}
}, "一號線程").start();
new Thread(new Runnable() {
@Override
public void run() {
lock.lock();
System.out.println("我是" + Thread.currentThread().getName() + ",lock在我手中");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
System.out.println(Thread.currentThread().getName() + "不須要lock了");
}
}
}, "二號線程").start();
}
複製代碼
輸出結果:
我是一號線程,lock在我手中
一號線程不須要lock了
我是二號線程,lock在我手中
二號線程不須要lock了
複製代碼
同窗們能夠用各類姿式來run咱們的代碼,不關你是坐着run,躺着run仍是倒立run,結果始終是連續的,不會出現一號線程和二號線程交替打印的情景。這就證實了sleep
確實不會釋放其獲取的監視鎖,可是他會放棄CPU執行權。實踐也實踐完了,可是每次都要計算毫秒也着實費勁,有沒有什麼好的辦法呢?
⚠️會玩的都這麼寫
假如如今有需求要求咱們讓線程sleep
1小時28分19秒33毫秒咱們要咋辦?手腳快的同窗可能已經掏出了祖傳的計算器滴滴滴地操做起來了。可是咱們通常不這麼作,JDK1.5爲咱們新增了一個TimeUnit
枚舉類,請你們收起心愛的計算器,其實咱們能夠這麼寫
//使用TimeUnit枚舉類
TimeUnit.HOURS.sleep(1);
TimeUnit.MINUTES.sleep(28);
TimeUnit.SECONDS.sleep(19);
TimeUnit.MILLISECONDS.sleep(33);
複製代碼
這樣咱們的代碼更加優雅,可讀性會更強
寫累了,鍛鍊下身體,給同窗們挖個坑。咱們已經知道millis
的範圍是大於等於0,sleep(1000)
咱們知道是什麼意思,那麼sleep(0)
會有做用嗎?
sleep
的第二個點,
sleep(0)的做用是「觸發操做系統馬上從新進行一次CPU競爭
,競爭結果多是當前線程繼續獲取到CPU的執行權,也有多是別的線程獲取到了當前線程的執行權。
兩個點但願你們能夠牢記
1.sleep不會釋放mointor lock。
2.sleep的做用是觸發操做系統馬上從新進行一次CPU競爭。
仍是老套路,咱們先來看API接口描述是怎麼定義這個接口的
/**
* A hint to the scheduler that the current thread is willing to yield
* its current use of a processor. The scheduler is free to ignore this
* hint.
*
* <p> Yield is a heuristic attempt to improve relative progression
* between threads that would otherwise over-utilise a CPU. Its use
* should be combined with detailed profiling and benchmarking to
* ensure that it actually has the desired effect.
*
* <p> It is rarely appropriate to use this method. It may be useful
* for debugging or testing purposes, where it may help to reproduce
* bugs due to race conditions. It may also be useful when designing
* concurrency control constructs such as the ones in the
* {@link java.util.concurrent.locks} package.
*/
public static native void yield();
複製代碼
這個方法的描述是這樣的提示調度程序,當前線程願意放棄CPU執行權。調度程序能夠無條件忽略這個提示,打個比方就是說,A暗戀B,A說我願意怎麼怎麼樣,B能夠接受A,可是也能夠徹底無條件的忽略A,,嗯嗯額~大概就是這麼個場景,卑微A。
API接口描述中也明確說明了,這個接口不經常使用,可能用於調試或測試的目的,可能用於重現因爲競爭條件而致使的bug,還有就是在java.util.concurrent.locks
包中有用到這個API,總的來講就是在實際生產開發過程當中是不用的。可是它又不像stop
同樣已經被廢棄不推薦使用,講這個API的目的是應由於它很容易和sleep
混淆。
1.調用yield
並生效以後線程會從RUNNING階段轉變爲RUNNABLE,固然被無條件忽略的狀況除外。而sleep
則是進入BLOCKED階段,並且是幾乎百分百會進入。
2.sleep
會致使線程暫停,可是不會消耗CPU時間片,yield
一旦生效就會發生線程上下文切換,會帶來必定的開銷。
3.sleep
能夠被另外一個線程調用interrupt
中斷,而yield
就不會,yield
得等到CPU輪詢給到執行權的時候纔會再次被喚醒,也就是從RUNNABLE
階段編程RUNNING
階段。
光說不練假把式,雖然不經常使用,可是是驢子是馬總歸仍是要溜一溜。
private static class MyYield implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
if (i % 5 == 0) {
System.out.println(Thread.currentThread().getName()+"線程,yield 它出現了");
// Thread.yield();
}
}
System.out.println(Thread.currentThread().getName()+"結束了");
}
}
public static void main(String[] args) {
Thread t1 = new Thread(new MyYield());
t1.start();
Thread t2 = new Thread(new MyYield());
t2.start();
Thread t3 = new Thread(new MyYield());
t3.start();
}
複製代碼
屢次執行,按到最多的輸出是連續的,相似下面這種輸出結果:
Thread-0線程,yield 它出現了
Thread-0結束了
Thread-1線程,yield 它出現了
Thread-1結束了
Thread-2線程,yield 它出現了
Thread-2結束了
複製代碼
如今咱們把註釋打開,發現輸出結果變了
Thread-0線程,yield 它出現了
Thread-1線程,yield 它出現了
Thread-2線程,yield 它出現了
Thread-0結束了
Thread-1結束了
Thread-2結束了
複製代碼
那是應爲在調用到yield
到時候當前線程讓出了執行權,因此等到你們都出現了
以後,你們再分別結束了
。
在本小節中咱們介紹一下join
API
sleep
API十分相像,可是join
除了兩個設置超時等待時間的API外,還額外提供了一個不設置超時時間的方法,可是經過追蹤第一個API咱們發現內部其實調用的就是第二個API的join(0)
,設置納秒的內部調用也是第二個API。因此咱們這邊就拿第二個API來說解。/**
//設置一段時間等待當前線程結束,若是超時還未返回就會一直等待
* Waits at most {@code millis} milliseconds for this thread to
* die. A timeout of {@code 0} means to wait forever.
*
這個方法調用的前提就是當前線程仍是處於alive狀態的
* <p> This implementation uses a loop of {@code this.wait} calls
* conditioned on {@code this.isAlive}. As a thread terminates the
* {@code this.notifyAll} method is invoked. It is recommended that
* applications not use {@code wait}, {@code notify}, or
* {@code notifyAll} on {@code Thread} instances.
*
* @param millis
* the time to wait in milliseconds
*
//超時時間爲負數
* @throws IllegalArgumentException
* if the value of {@code millis} is negative
*
* @throws InterruptedException
* if any thread has interrupted the current thread. The
* <i>interrupted status</i> of the current thread is
* cleared when this exception is thrown.
*/
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
複製代碼
經過該方法咱們能夠看到他的邏輯是經過當前運行機器的時間,判斷線程是否isAlive
來決定是否須要繼續等待,而且內部咱們能夠看到調用的是wait()
方法,直到delay<=0
的時刻,就會跳出當前循環,從而結束中斷。
又要給同窗們講一個悲傷的故事了
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
System.out.println("週末都要加班,終於回家了,洗個手吃飯了");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print("洗完手,");
});
Thread t2 = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(2);
// t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print("拿起筷子,");
});
t1.start();
t2.start();
// t2.join();
System.out.println("我要吃飯了");
}
複製代碼
輸出:
週末都要加班,終於回家了,洗個手吃飯了
我要吃飯了
拿起筷子,洗完手,
複製代碼
顯然這個結果不是咱們想要的結果,也可是不排除加班加的已經意識模糊,手抓飯了,這裏咱們仍是但願按照正常習慣來執行。咱們把註釋打開
輸出:
週末都要加班,終於回家了,洗個手吃飯了
洗完手,拿起筷子,我要吃飯了
複製代碼
這個纔是咱們須要的結果。
相信同窗們經過這個例子已經大概瞭解join
的做用了,沒錯join
是可讓程序能按照必定的次序來完成咱們想完成的工做,他的工做原理就是阻塞當前調用join
的線程,讓新join
進來的線程優先執行。
線程關閉大體上能夠分爲三種狀況
1.線程正常關閉
2.線程異常退出
3.進程假死
這裏咱們着重講一下線程正常關閉的狀況,也是實際開發生產中經常使用方法。
這個沒什麼好說的,就是線程邏輯單元執行完成而後本身正常結束。
早期JDK中還提供有一個stop
函數用於關閉銷燬線程,可是後來發現會存在monitor鎖沒法釋放的問題,會致使死鎖,因此如今強烈建議你們不要用這個方式。這裏咱們使用捕獲線程中斷的方式來結束線程。
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
System.out.println("我要自測代碼啦~~");
while (!Thread.currentThread().isInterrupted()) {
System.out.println("目前來看是好好的");
}
System.out.println("代碼中斷中止了");
});
t1.start();
TimeUnit.SECONDS.sleep(1);
t1.interrupt();
}
複製代碼
輸出:
我要自測代碼啦~~
...
目前來看是好好的
目前來看是好好的
代碼中斷中止了
複製代碼
能夠看到,咱們經過判斷當前線程的isInterrupted()
狀態來捕獲線程是否已經被中斷,從而能夠來控制線程正常關閉。同理,若是咱們在線程內部已經執行來某中斷方法,好比sleep
就能夠經過捕獲中斷異常來退出sleep
狀態,從而也能讓線程正常結束。
因爲interrupt
頗有可能被擦除,或者整個邏輯單元中並有調用中斷方法,這樣咱們上一種方法就不適用了,這裏咱們使用volatile關鍵字來設置一個開關,控制線程的正常退出。
private static class MyInterrupted extends Thread {
private volatile boolean close = false;
@Override
public void run() {
System.out.println("我要開始自測代碼啦~~");
while (!close) {
System.out.println("目前來看好好的");
}
System.out.println("close已經變成了" + close + ",代碼正常關閉了");
}
public void closed() {
this.close = true;
}
}
public static void main(String[] args) throws InterruptedException {
MyInterrupted myInterrupted = new MyInterrupted();
myInterrupted.start();
TimeUnit.SECONDS.sleep(1);
System.out.println("我要開始關閉線程了");
myInterrupted.closed();
}
複製代碼
輸出:
我要開始自測代碼啦~~
...
目前來看好好的
目前來看好好的
目前來看好好的
我要開始關閉線程了
close已經變成了true,代碼正常關閉了
複製代碼
能夠看到咱們調用closed方法時候把close設置爲了true,從而正常關閉代碼,關於volatile
關鍵字咱們以後的章節會詳細講哦,請同窗們繼續關注,和我一塊兒學習😁,但願能夠同窗們幫忙關注下和點點贊👍哦~
下一章也已經出來咯,此次內容稍多,因此拖了比較久,可是內容都是妥妥的,下一章詳細講解了interrupt的執行邏輯,一步一步帶同窗們調試,感興趣的同窗記得進入下一章的學習哦~