前面幾篇文章為Java多線程做足了鋪墊,這篇終于到了正題,一起學(xué)習(xí)一下Java多線程的基礎(chǔ)知識。
1. Java線程生命周期
下圖表述了Java線程的幾個基本狀態(tài):

- New新建狀態(tài):線程創(chuàng)建后即進入。
- Runnable就緒狀態(tài):調(diào)用線程的start方法后,線程不會立刻運行,而是進入就緒狀態(tài),等待CPU調(diào)度。
- Running運行狀態(tài): CPU開始調(diào)度處于就緒狀態(tài)的線程后,線程進入運行狀態(tài)。<font color="red">換言之,就緒狀態(tài)是運行狀態(tài)的唯一入口。</font>
- Blocked阻塞狀態(tài):線程由于某種原因放棄CPU的使用權(quán),停止執(zhí)行,此時進入阻塞狀態(tài),一直到進入就緒態(tài)CPU才能對其進行重新調(diào)度。
產(chǎn)生Blocked狀態(tài)的幾種可能
等待阻塞:運行線程中的wait()方法,使本地線程進入阻塞狀態(tài)。
同步阻塞:線程獲取sychronized同步鎖失敗,進入同步阻塞狀態(tài)。
其他阻塞:調(diào)用線程的sleep()或join()發(fā)出I/O請求,線程進入阻塞狀態(tài)。當(dāng)sleep()超時,join()等待線程終止或超時,處理完I/O時,線程重新進入就緒狀態(tài)。
- Dead死亡狀態(tài):線程執(zhí)行完畢或異常退出。該線程結(jié)束了生命周期。
2. Java線程實現(xiàn)方法
2.1. 繼承Thread類,重寫run()方法
最基本的線程方式,通過繼承Thread類,重寫run()方法即可運行子線程。缺點是Java類只能有一個父類,如果所要使用的類本身有其他需要繼承的實體,就會比較麻煩。
class MyThread extends Thread {
private volatile boolean isRunning = true;
public boolean isRunning() {
return isRunning;
}
public void setRunning(boolean isRunning) {
this.isRunning = isRunning;
}
@Override
public void run() {
System.out.println("進入到run方法中了");
while (isRunning == true) {
}
System.out.println("線程執(zhí)行完成了");//結(jié)束線程,進入dead態(tài)
}
}
public class RunThread{
public static void main(String[] args) {
try {
MyThread thread = new MyThread();//創(chuàng)建線程,進入new態(tài)
thread.start();//start線程,進入runnable態(tài),CPU開始調(diào)度后進入running態(tài)
Thread.sleep(1000);//阻塞主線程,進入blocked態(tài)
thread.setRunning(false);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
2.2. 實現(xiàn)Runnable接口,便于繼承其他類
針對Thread類繼承可能存在困難的問題,利用Java接口無限制的特性,利用接口來進行線程實現(xiàn)。在運行時,需要運行一個thread并把接入了Runnable的類以參數(shù)形式傳給thread。
class MyClass implements Runnable {
private volatile boolean isRunning = true;
public boolean isRunning() {
return isRunning;
}
public void setRunning(boolean isRunning) {
this.isRunning = isRunning;
}
@Override
public void run() {
System.out.println("進入到run方法中了");
while (isRunning == true) {
}
System.out.println("線程執(zhí)行完成了");
}
}
public class RunThread{
public static void main(String[] args) {
try {
MyClass myClass=new MyClass();
Thread thread=new Thread(myClass);
thread.start();
Thread.sleep(1000);
myClass.setRunning(false);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
我們需要知道的是,Thread類本身就已經(jīng)接入了Runnable接口,如果我們給一個自定義的已經(jīng)重寫run的Thread類的子類A傳入一個接入Runnable的類B作為參數(shù),那么該子類A會運行類B的run()函數(shù),原因為,Thread中的run()函數(shù)代碼如下:
@Override
public void run() {
if (target != null) {
target.run();
}
}
可見,如果target存在,即有Runnable類的子類作為參數(shù)傳遞,則不運行Thread提供的run()方法。
最基本的Thread和Runnable有一個共同的問題,即是run()函數(shù)沒有返回值,導(dǎo)致線程間交互會比較復(fù)雜,需要依賴引用的互相傳遞。因此,當(dāng)需要有返回值時,Java提供了另外的類。
2.3. Callable類替換Runnable類,實現(xiàn)返回值
Callable接口定義如下:
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}
對于Callablel類我們只需要知道以下幾點
需要實現(xiàn)重寫其call()函數(shù)來實現(xiàn)功能。
泛型的輸入類型即是call()的返回類型。
實現(xiàn)了call函數(shù),簡單的把Callable接口替換Runnable接口,我們就可以實現(xiàn)簡答的有返回值線程了,但Java提供了更多的線程狀態(tài)檢查,控制類來更好的使用這兩個接口。
2.4. Future接口對任務(wù)進行監(jiān)測
Future接口有5個方法,我們依次來進行介紹
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
- cancel() 方法來表示取消任務(wù),如果取消成功則返回true,否則返回false。其參數(shù)mayInterruptIfRunning則表示正在執(zhí)行的任務(wù)。
- isCanelled() 方法表示任務(wù)是否已經(jīng)取消,如果任務(wù)完成前成功取消則返回true。
- isDone() 方法表示任務(wù)是否已經(jīng)完成,如果完成返回true。
- get() 方法獲取執(zhí)行結(jié)果,這個方法會產(chǎn)生阻塞,一直到執(zhí)行完畢才返回。
- get(long timeout, TimeUnit unit) 方法在獲取執(zhí)行結(jié)果同時給一個時間,如果超時仍被阻塞則返回Null。
2.5. FutureTask類:Future類的唯一實現(xiàn)
FutureTask類實現(xiàn)了RunnableFuture接口,而RunnableFuture接口如同名字一樣,接入了Runnable和Future兩個類。因此FutrueTask既可以作為Runnable被線程執(zhí)行(自動調(diào)用其run()方法),同時又可以視為Future類來得到Callable類的返回值。此處我們可以看到Java設(shè)置這個類的用意——讓用戶盡量使用這個類去管理線程。
使用FutureTask類托管Callable的一個例子:
class MyClass implements Callable {
private volatile boolean isRunning = true;
public boolean isRunning() {
return isRunning;
}
public void setRunning(boolean isRunning) {
this.isRunning = isRunning;
}
@Override
public Object call() {
System.out.println("進入到run方法中了");
while (isRunning == true) {
}
System.out.println("線程執(zhí)行完成了");
return isRunning;
}
}
public class RunThread{
public static void main(String[] args) {
try {
MyClass myClass=new MyClass();
FutureTask futureTask=new FutureTask(myClass);
Thread thread=new Thread(futureTask);
thread.start();
Thread.sleep(1000);
myClass.setRunning(false);
try{
System.out.println(futureTask.get());
}
catch (Exception e){
System.out.println("Type error");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
3. Java多線程的調(diào)度
3.1. Java多線程的就緒,運行和死亡

如圖,需要注意的是CPU調(diào)度線程有隨機性,不排除線程調(diào)用yield()進入就緒狀態(tài)后CPU又去調(diào)用線程的情況。
3.2. Java線程的阻塞
3.2.1. Java線程的阻塞方法
- join()方法:讓一個線程等待另一個線程完成才繼續(xù)執(zhí)行。如,在線程A的運行過程中調(diào)用線程B的join方法,則線程A被阻塞,等待線程B運行完成后才繼續(xù)運行。該方法通常用于主線程中。
public static void main(String[] args){
......
threadA.join();
}
- sleep()方法:讓當(dāng)前的線程進入阻塞狀態(tài),暫停指定時間,之后恢復(fù)就緒狀態(tài)。例如,在前面的例子里,我們讓主線程sleep 1000毫秒以顯示效果。需要注意的是,sleep()方法并不會釋放鎖,實際上,sleep()是一個與實例無關(guān)的方法。sleep()方法是一個靜態(tài)方法,也就是說他只對當(dāng)前線程有效,通過t.sleep()讓t對象進入sleep,這樣的做法是錯誤的,其效果和Thread.sleep()無異但造成了一定迷惑性。sleep()的目的在于讓當(dāng)前線程自主的暫停,因而是針對當(dāng)前線程生效的靜態(tài)方法,即是在子線程的run()函數(shù)中也可以調(diào)用。如果不是這樣的設(shè)計,sleep()要么和wait()同質(zhì),要么會造成嚴(yán)重的死鎖。與sleep()方法有一定共性但實質(zhì)上區(qū)別很大的wait()方法接下來我會單開文章去談。
3.3. 后臺線程
后臺線程主要為其他線程提供服務(wù),或“守護線程”。例如:JVM的GC線程。后臺線程與前臺線程生命周期有一定關(guān)聯(lián)。主要體現(xiàn)在:當(dāng)所有前臺線程死去時,后臺線程也會自動死去。
設(shè)置一個線程是否為后臺線程只需要調(diào)用該線程的setDaemon(boolean val)方法即可,創(chuàng)建一個線程后,默認(rèn)為非后臺線程。
public static void main(String[] args){
......
threadA.setDaemon(true);
}
3.4. 線程的優(yōu)先級
考慮到下面的情景,5個線程競爭同一資源,資源目前被重要線程A鎖定,當(dāng)線程A執(zhí)行完畢,解鎖釋放資源后,我們希望重要線程E能先于BCD得到資源。此時,我們則需要優(yōu)先級這個屬性。每個線程都有一個優(yōu)先級變量,該變量的值可以是1到10之間的一個常數(shù)。數(shù)字越大,代表優(yōu)先級越高。Java給了其中三個常量名稱,分別是:
MAX_PRIORITY:10
MIN_PRIORITY:1
NORM_PRIORITY:5
默認(rèn)情況下,一個線程的優(yōu)先級是其父線程的優(yōu)先級。而主線程的優(yōu)先級是NORM_PRIORITY,即數(shù)字5,因此,我們可以看到很多資料里認(rèn)為現(xiàn)成的默認(rèn)優(yōu)先級是5,其實不是這樣的。我們可以通過調(diào)用線程的setPrority(short val)來設(shè)置線程的優(yōu)先級以及使用getPrority()來獲取優(yōu)先級。
class MyClass implements Callable {
@Override
public Object call() {
System.out.println("線程執(zhí)行完成了,將返回其子線程的優(yōu)先級");
return new Thread().getPriority();
}
}
public class RunThread {
public static void main(String[] args) {
FutureTask futureTask = new FutureTask(new MyClass());
Thread thread = new Thread(futureTask);
thread.setPriority(3);
System.out.println("設(shè)置該線程的優(yōu)先級為"+thread.getPriority());
thread.start();
try {
System.out.println("該線程的子線程優(yōu)先級為" + futureTask.get());
} catch (Exception e) {
System.out.println("Type error");
}
}
}
上面這段代碼的返回內(nèi)容如下:
設(shè)置該線程的優(yōu)先級為3
線程執(zhí)行完成了,將返回其子線程的優(yōu)先級
該線程的子線程優(yōu)先級為3
然而,需要注意的是,優(yōu)先級無法保證線程的執(zhí)行順序,即在BCDE競爭資源的情境中,無法保證E線程一定先執(zhí)行。優(yōu)先級高則其獲取CPU資源的可能性比較大,而不是一定會先于其他線程獲取執(zhí)行優(yōu)先權(quán),優(yōu)先級低的線程仍有得到執(zhí)行權(quán)的可能。
同時我們不要忘記,Java的Thread類的核心方法是native方法,即是線程實現(xiàn)大量依賴宿主機的機制。Java有10個優(yōu)先級,如果宿主機是Linux系統(tǒng),有31個優(yōu)先級,顯然沒什么問題。但如果宿主機是Windows7,只有7個優(yōu)先級,那么Java的10個優(yōu)先級就會根據(jù)系統(tǒng)的優(yōu)先級進行映射。同時,線程的優(yōu)先級也會受到虛擬機的影響。
3.5. 線程讓步y(tǒng)ield()
前面多少也提到過了。yield()和sleep()類似,是靜態(tài)方法,作用于將當(dāng)前運行的線程,其作用于具體線程,調(diào)用方法是Thread.yield(),使用t.yield()同樣是能夠使用但邏輯不清晰的做法。具體來說。yield()是讓當(dāng)前線程從running態(tài)回到runnable態(tài)。之后,線程間重新進行資源競爭,由CPU進行調(diào)度,因此yield()實際上是一個與鎖不是很有關(guān)系的方法,和sleep()一樣,作為當(dāng)前線程的主動方法,不會引起死鎖。