摘要:如果線程還存活,線程就無限期等待,并讓出監視器鎖,進入狀態。當線程從狀態被喚醒后通過,或者是假喚醒將繼續競爭監視器鎖,當成功獲得監視器鎖后,他將從調用的地方恢復,繼續運行。
前言
系列文章目錄
上一篇我們討論了線程的創建,本篇我們來聊一聊線程的狀態轉換以及常用的幾個比較重要的方法。
本篇依然是通過源碼分析來了解這些知識。
本文源碼基于jdk1.8 。
閱讀完本文,你應當有能力回答以下常見面試題:
線程有哪幾種狀態以及各種狀態之間的轉換?
Thread.sleep() 與 Thread.currentThread().sleep() 有什么區別?
Thread.sleep() 和 Object#wait() 有什么區別?
Thread.sleep() 和 Thread.yield()有什么區別?
說說你對join方法的理解?
線程狀態在Thread類中, 線程狀態是通過threadStatus屬性以及State枚舉類實現的:
/* Java thread status for tools, * initialized to indicate thread "not yet started" */ private volatile int threadStatus = 0; public enum State { /** * Thread state for a thread which has not yet started. */ NEW, /** * Thread state for a runnable thread. A thread in the runnable * state is executing in the Java virtual machine but it may * be waiting for other resources from the operating system * such as processor. */ RUNNABLE, /** * Thread state for a thread blocked waiting for a monitor lock. * A thread in the blocked state is waiting for a monitor lock * to enter a synchronized block/method or * reenter a synchronized block/method after calling * {@link Object#wait() Object.wait}. */ BLOCKED, /** * Thread state for a waiting thread. * A thread is in the waiting state due to calling one of the * following methods: *
A thread in the waiting state is waiting for another thread to * perform a particular action. * * For example, a thread that has called Object.wait() * on an object is waiting for another thread to call * Object.notify() or Object.notifyAll() on * that object. A thread that has called Thread.join() * is waiting for a specified thread to terminate. */ WAITING, /** * Thread state for a waiting thread with a specified waiting time. * A thread is in the timed waiting state due to calling one of * the following methods with a specified positive waiting time: *
從源碼中可以看出, 線程一共有6種狀態, 其狀態轉換關系如下圖所示:
值得一提的是,從狀態的定義中可以看出,RUNNABLE狀態包含了我們通常所說的running和ready兩種狀態。
常用方法 currentThread源碼中currentThread定義如下:
/** * Returns a reference to the currently executing thread object. * * @return the currently executing thread. */ public static native Thread currentThread();
可見,它是一個靜態方法,并且是一個native方法,返回的是當前正在執行的線程。
愛思考的同學可能就要問了,現在咱都多核CPU了,同一時刻可以有多個線程跑在不同的CPU核心上,那當前正在執行的線程有多個,到底返回的是哪一個呢?
其實,這里"當前正在執行的線程"指的是當前正在執行這段代碼的線程。
我們知道,線程是CPU調度的最小單位,任意一段代碼總得由一個線程執行,所以該方法返回的是正在執行Thread.currentThread這行代碼的線程,例如:
public class ThreadTest { public static void main(String[] args) { System.out.println(Thread.currentThread().getName()); } }
輸出:
main
我們知道當一個Java程序啟動以后,有一個線程就會立馬跑起來,這就是通常所說的Main線程,main線程將會執行java的入口方法main方法,所以當前正在執行Thread.currentThread()方法的線程就是main線程。
sleep談起sleep方法, 被問的最多的兩個問題就是:
Thread.sleep() 與 Thread.currentThread().sleep() 有什么區別?
Thread.sleep() 和 Object.wait()有什么區別?
這些問題的答案, 你在源碼里都能找得到。我們直接來看源碼:
/** * Causes the currently executing thread to sleep (temporarily cease * execution) for the specified number of milliseconds, 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 * * @throws IllegalArgumentException * if the value of {@code millis} is negative * * @throws InterruptedException * if any thread has interrupted the current thread. The * interrupted status of the current thread is * cleared when this exception is thrown. */ public static native void sleep(long millis) throws InterruptedException;
可見, sleep方法也是一個靜態方法, 并且是native方法, 從注釋Causes the currently executing thread to sleep中可以看出, 它作用于當前正在執行的線程, 所以上面那個問題我們就能回答了:
Thread.sleep() 與 Thread.currentThread().sleep() 沒有區別
如果硬要說他們有什么區別的話, 那就是一個是用類直接調用靜態方法, 一個是用類的實例調用靜態方法.
另外, 上面的注釋中還有一句非常重要的話:
The thread does not lose ownership of any monitors.
也就是說, 雖然sleep函數使當前線程讓出了CPU, 但是, 當前線程仍然持有它所獲得的監視器鎖, 這與同時讓出CPU資源和監視器鎖資源的wait方法是不一樣的。
sleep方法還有另外一個版本:
/** * 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 * interrupted status 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); }
這個方法多加了納秒級別的延時參數, 但是我們看源碼就知道, 這個多加的納秒級別的延時并沒有什么用, 最終該函數還是調用了上面的單參數native sleep方法, 延時還是毫秒級別的, 多出來的參數最多是讓當前毫秒級別的延時增加1毫秒.
還記得我們上次講的wait方法嗎?我們來對比下:
public final void wait(long timeout, int nanos) throws InterruptedException { if (timeout < 0) { throw new IllegalArgumentException("timeout value is negative"); } if (nanos < 0 || nanos > 999999) { throw new IllegalArgumentException("nanosecond timeout value out of range"); } if (nanos > 0) { timeout++; } wait(timeout); }
怎么樣?是不是很像?兩者只不過在從納秒向毫秒的進位處有細微的差別,我猜這個不統一是歷史原因導致的。
另外,值得一提的是,wait有無參的wait()方法,它調用的是wait(0),表示無限期等待,而sleep并沒有無參數的版本,那么sleep(0)代表什么呢?
這一點在源碼里面并沒有提及,但是通過猜測sleep方法的定義我們知道,它是讓出CPU 0毫秒,這聽上去好像沒有什么意義,但其實調用Thread.sleep(0)的當前線程確實被“凍結”了一下,讓其他線程有機會優先執行。也就是說當前線程會釋放一些未用完的時間片給其他線程或進程使用,就相當于一個讓位動作,這看上去就和下面要說的yield方法很像了。
yield既然上面談到了sleep(0)方法, 就不得不提yield方法了:
/** * 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. * *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. * *
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();
yield方法也是一個native方法, 從它的注釋可以看出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. 它對于CPU只是一個建議, 告訴CPU, 當前線程愿意讓出CPU給其他線程使用, 至于CPU采不采納, 取決于不同廠商的行為, 有可能一個線程剛yield出CPU, 然后又立馬獲得了CPU。與之相對, sleep方法一定會讓出CPU資源, 并且休眠指定的時間, 不參與CPU的競爭.
所以調用yield方法不會使線程退出RUNNANLE狀態,頂多會使線程從running 變成 ready,
但是sleep方法是有可能將線程狀態轉換成TIMED_WAITING的。
isAlive方法用于檢查線程是否還活著,它是一個native方法,但不是靜態方法,也就是說它必須被線程的實例所調用。
其實大家可以思考一下它為什么不是靜態方法,因為靜態方法一般都是作用于當前正在執行的線程,既然是“當前正在執行”,那必然是Alive的,所以作為靜態方法調用并沒有意義。
/** * Tests if this thread is alive. A thread is alive if it has * been started and has not yet died. * * @returnjointrue
if this thread is alive; *false
otherwise. */ public final native boolean isAlive();
join方法是另一個能將線程狀態轉換成WAITING或者TIMED_WAITING的,它和wait方法一樣,有三個版本,我們一個個來看:
/** * Waits at most {@code millis} milliseconds for this thread to * die. A timeout of {@code 0} means to wait forever. * *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 * interrupted status 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; } } }
這段源碼注釋的開頭部分就告訴了我們join方法的作用:
Waits at most {@code millis} milliseconds for this thread to die. A timeout of {@code 0} means to wait forever.
也就是說,該方法等待this thread終止,最多等指定的時間,如果指定時間為0,則一直等。
這里有兩個問題需要弄清楚:
誰在等this thread終止?
this thread指的是哪個線程?
為了便于說明,我們直接來看一個例子:
public class JoinMethodTest { private static void printWithThread(String content) { System.out.println("[" + Thread.currentThread().getName() + "線程]: " + content); } public static void main(String[] args) { printWithThread("開始執行main方法"); Thread myThread = new Thread(() -> { printWithThread("我在自定義的線程的run方法里"); printWithThread("我馬上要休息1秒鐘, 并讓出CPU給別的線程使用."); try { Thread.sleep(1000); printWithThread("已經休息了1秒, 又重新獲得了CPU"); printWithThread("我休息好了, 馬上就退出了"); } catch (InterruptedException e) { e.printStackTrace(); } }); try { myThread.start(); printWithThread("我在main方法里面, 我要等下面這個線程執行完了才能繼續往下執行."); myThread.join(); printWithThread("我在main方法里面, 馬上就要退出了."); } catch (InterruptedException e) { e.printStackTrace(); } } }
在上面的例子中,我們在main方法中調用了 myThread.join(),注意上面這段代碼有兩個線程,一個是執行main方法的線程,一個是我們自定義的myThread線程,所以上面的兩個問題的答案是:
main線程在等this thread的終止,因為我們在main方法中調用了myThread.join()
this thread線程指的是myThread線程,因為我們在myThread對象上調用了join方法。
上面這段代碼的執行結果為:
[main線程]: 開始執行main方法 [main線程]: 我在main方法里面, 我要等下面這個線程執行完了才能繼續往下執行. [Thread-0線程]: 我在自定義的線程的run方法里 [Thread-0線程]: 我馬上要休息1秒鐘, 并讓出CPU給別的線程使用. [Thread-0線程]: 已經休息了1秒, 又重新獲得了CPU [Thread-0線程]: 我休息好了, 馬上就退出了 [main線程]: 我在main方法里面, 馬上就要退出了.
從運行結果可以看出,雖然myThread線程(即Thread-0線程)中途讓出了CPU, main線程還是必須等到其執行完畢了才能繼續往下執行,我們現在修改一下代碼,讓main線程最多等0.5秒,即將myThread.join()改為myThread.join(500);,則結果如下:
[main線程]: 開始執行main方法 [main線程]: 我在main方法里面, 我要等下面這個線程執行完了才能繼續往下執行. [Thread-0線程]: 我在自定義的線程的run方法里 [Thread-0線程]: 我馬上要休息1秒鐘, 并讓出CPU給別的線程使用. [main線程]: 我在main方法里面, 馬上就要退出了. [Thread-0線程]: 已經休息了1秒, 又重新獲得了CPU [Thread-0線程]: 我休息好了, 馬上就退出了
我們看到,由于main線程最多等待myThread 0.5秒,在myThread休眠的一秒內,它就不等了,繼續往下執行,而隨后myThread搶占到CPU資源繼續運行。
通過列子有了感性的認識后,我們再來看源碼,首先看join(0)部分:
public final synchronized void join(long millis) throws InterruptedException { ... if (millis == 0) { while (isAlive()) { wait(0); } } else { ... } ... }
這是一個自旋操作,注意,這里的isAlive和wait(0)方法都是線程實例的方法,在上面的例子中就是myThread的方法,Thread雖然是一個線程類,但只是特殊在它的native方法上,除此之外,它就是個普通的java類,而java中所有的類都繼承自Object類,所以Thread類繼承了Object的wait方法,myThread作為線程類的實例,自然也有wait方法。
我們之前說wait方法的時候提到過,執行wait方法必須拿到監視器鎖,并且必須在同步代碼塊中調用,這里我們檢查join方法發現,它確實被synchronized關鍵字修飾,并且是一個非靜態方法,所以它使用的是當前對象實例的監視器鎖(this)。
好像開始復雜了,我們從頭到尾捋一捋(注意了!敲黑板了!這段比較繞! ):
首先我們要明確,這里牽涉到兩個線程,一個是main線程,一個是我們自定義的myThread線程(即例子里的Thread-0)。
我們在main方法中調用了myThread.join(),main方法由main線程執行,所以執行myThread.join()這行代碼的“當前線程”是main線程。
join方法是一個同步方法,使用的是對象鎖(this 鎖),即myThread對象所關聯的監視器對象。
main線程必須首先拿到join方法的監視器鎖才能進入同步代碼塊。
main線程進入同步代碼塊后會首先檢查myThread線程是否還存活,注意,這里的isAlive是myThread線程的方法,它是檢查myThread線程是否還活著,而不是當前線程(當前線程是執行isAlive方法的線程,即main線程)。
如果myThread線程還存活,(main線程)就無限期等待,并讓出監視器鎖,進入WAITING狀態。
當main線程從WAITING狀態被喚醒后(通過notify,notifyAll或者是假喚醒), 將繼續競爭監視器鎖,當成功獲得監視器鎖后,他將從調用wait的地方恢復,繼續運行。由于wait方法在while循環中,則它將繼續檢查myThread線程是否存活,如果還是沒有終止,則繼續掛起等待。
可以看出,退出這個“自旋”狀態的唯一途徑就是myThread線程終止運行(或者有中斷異常拋出)。
有的細心的同學可能就要問了: 要是沒有人調用notify或者notifyAll,也沒有假喚醒狀態的發生,那main線程不就一直被wait(0)方法掛起了嗎?這樣以來不就連檢測myThread線程是否存活的機會都沒有嗎?這樣即使myThread終止了,也無法退出啊。
關于這一點,注釋中其實是做了解釋的:
As a thread terminates the {@code this.notifyAll} method is invoked.
我們知道,wait(0)方法的監視器鎖就是myThread對象(this), 而當myThread終止執行時,this.notifyAll會被調用,所以所有等待this鎖的線程都會被喚醒,而main線程就是等待在這個監視器鎖上的線程,因此myThread運行結束時,main線程會從wait方法處被喚醒。
另外,注釋中還多加了一句:
It is recommended that applications not use {@code wait}, {@code notify}, or {@code notifyAll} on {@code Thread} instances.
這個推薦還是很有必要的,至于為什么,就給大家留作思考題吧<( ̄︶ ̄)>
不過我這里再啰嗦一句,一定要分清執行代碼的線程和方法所屬的線程類所代表的線程!
例如,在上面的例子中:
myThread.join() 是myThread對象的方法,但是執行這個方法的是main線程;
isAlive() 是myThread對象的方法,但是執行這個方法的是main線程,而這個方法檢測是myThread線程是否活著
wait(0) 是myThread對象的方法,但是執行這個方法的是main線程,它使得main線程掛起,但是main線程是在myThread對象代表的monitor上掛起。
這里最重要的是區分“myThread對象”和“myThread線程”,myThread對象有時候代表了myThread線程,例如myThread對象的isAlive方法,檢測的就是它代表的myThread線程是否活著,但是其實大多數時候,myThread對象就是普通的java對象,這個對象的方法通常也都是由其他線程(例如上面例子中的main線程)來執行的,對于我們自定義的線程來說(例如上面的myThread線程),通常由它自己執行的方法就只有傳進入的run方法了。
再回到上面的例子,從上面的分析中可以看出,join(0)方法實現了一定程度上的線程同步,即當前線程只有等join方法所屬的線程對象所代表的線程終止執行了才能繼續往下執行,否則將一直掛起等待。
這一點也說明使用join(0)是很危險的,因為如果myThread線程因為得不到資源一直被掛起,而main線程又在等待myThread線程終止,則程序永遠會停在那里,無法終止,所以源碼中提供了限時等待的版本:
public final synchronized void join(long millis) throws InterruptedException { long base = System.currentTimeMillis(); long now = 0; ... if (millis == 0) { ... } else { while (isAlive()) { long delay = millis - now; if (delay <= 0) { break; } wait(delay); now = System.currentTimeMillis() - base; } } }
與無限期等待不同的是,限時等待只等待指定時間,如果指定的時間到了就直接從循環中跳出來,使用的wai方法也是限時wait的版本,定時時間到了之后,main線程會被自動喚醒。上面的代碼是自解釋的,我就不再贅述了。
接下來我們再來看看其他兩個版本的join方法:
public final void join() throws InterruptedException { join(0); } public final synchronized void join(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++; } join(millis); }
可見,其他兩個版本最終調用的都是我們分析的第一版本,這和wait方法,sleep方法很像,至于為什么wait方法和join方法都提供了無參方法而sleep方法沒有,我個人認為是為了保持語義的一致性:
wait()和join()分別和wait(0)和join(0)等價,他們都代表了無限期等待,而sleep(0)并不代表無限期等待,所以sleep方法沒有無參的形式,以防止語義上的混亂。除這點之外,這三個方法在兩個參數的版本XXX(long millis, int nanos)中的實現,都大同小異。
另外最后一點值得注意的是,我們在join方法中只調用了isAlive方法檢測線程是否存活,并沒有啟動這個線程,也就是說,如果我們想要實現當前線程等待myThread線程執行完成之后再執行的效果,就必須在調用myThread.join()之前調用myThread.start()讓線程先跑起來,否則join方法發現isAlive為false會立即退出,myThread線程就不會被執行,大家可以將myThread.start()注釋掉自己跑一跑試試看。
(完)
查看更多系列文章:系列文章目錄
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/76779.html
摘要:為了避免一篇文章的篇幅過長,于是一些比較大的主題就都分成幾篇來講了,這篇文章是筆者所有文章的目錄,將會持續更新,以給大家一個查看系列文章的入口。 前言 大家好,筆者是今年才開始寫博客的,寫作的初衷主要是想記錄和分享自己的學習經歷。因為寫作的時候發現,為了弄懂一個知識,不得不先去了解另外一些知識,這樣以來,為了說明一個問題,就要把一系列知識都了解一遍,寫出來的文章就特別長。 為了避免一篇...
摘要:現在終止一個線程,基本上只能靠曲線救國式的中斷來實現。中斷機制的核心在于中斷狀態和異常中斷狀態設置一個中斷狀態清除一個中斷狀態方法同時會返回線程原來的中斷的狀態。中斷異常中斷異常一般是線程被中斷后,在一些類型的方法如中拋出。 前言 系列文章目錄 線程中斷是一個很重要的概念,通常,取消一個任務的執行,最好的,同時也是最合理的方法,就是通過中斷。 本篇我們主要還是通過源碼分析來看看中斷的概...
摘要:基礎問題的的性能及原理之區別詳解備忘筆記深入理解流水線抽象關鍵字修飾符知識點總結必看篇中的關鍵字解析回調機制解讀抽象類與三大特征時間和時間戳的相互轉換為什么要使用內部類對象鎖和類鎖的區別,,優缺點及比較提高篇八詳解內部類單例模式和 Java基礎問題 String的+的性能及原理 java之yield(),sleep(),wait()區別詳解-備忘筆記 深入理解Java Stream流水...
摘要:基礎問題的的性能及原理之區別詳解備忘筆記深入理解流水線抽象關鍵字修飾符知識點總結必看篇中的關鍵字解析回調機制解讀抽象類與三大特征時間和時間戳的相互轉換為什么要使用內部類對象鎖和類鎖的區別,,優缺點及比較提高篇八詳解內部類單例模式和 Java基礎問題 String的+的性能及原理 java之yield(),sleep(),wait()區別詳解-備忘筆記 深入理解Java Stream流水...
閱讀 1815·2019-08-30 13:54
閱讀 2728·2019-08-29 17:27
閱讀 1114·2019-08-29 17:23
閱讀 3354·2019-08-29 15:20
閱讀 1229·2019-08-29 11:28
閱讀 1570·2019-08-26 10:39
閱讀 1318·2019-08-26 10:29
閱讀 642·2019-08-26 10:13