Thread, Runable, Callable 還傻傻分不清?

Java技術(shù)棧
www.javastack.cn
關(guān)注閱讀更多優(yōu)質(zhì)文章
一、Thread 與 Runnable
1、創(chuàng)建線程的兩種方法
在java中你怎么創(chuàng)建線程?相信你很快能夠想到繼承Thread類和實現(xiàn)Runnable接口這兩種方式。
沒錯,java提供了這兩種方式來創(chuàng)建新的線程。
網(wǎng)上也有各種文章介紹這兩種方式創(chuàng)建線程的區(qū)別,但是我們這里要講的是這兩種方式的關(guān)聯(lián)。
先分別看看這兩種方式的代碼:
1、繼承Thread類,重寫run方法
public?class?MyThread?extends?Thread?{????
????
????@Override
????public?void?run()?{
????????System.out.println("我是繼承Thread類創(chuàng)建的線程喲");
????}????
????
????public?static?void?main(String[]?args)?{
????????MyThread?myThread?=?new?MyThread();
????????myThread.start();
????}
}
2、實現(xiàn)Runnable接口,實現(xiàn)run方法
public?class?MyRunnable?implements?Runnable?{????
????
????@Override
????public?void?run()?{
????????System.out.println("我是實現(xiàn)Runnable接口創(chuàng)建的線程喲");
????}????
????
????public?static?void?main(String[]?args)?{
????????MyRunnable?myRunnable?=?new?MyRunnable();????????
????????new?Thread(myRunnable).start();
????}
}
通過上面的代碼我們不難發(fā)現(xiàn),第一種方式中,繼承了Thread類的子類通過重寫父類的run方法就可以實現(xiàn)線程的創(chuàng)建。
而第二種方式中實現(xiàn)Runnable接口的類的對象可以作為一個參數(shù)傳遞到創(chuàng)建的thread對象中。那么Runnable為何方神圣?跟Thread類之間又有哪些不為人知的秘密?
2、Thread與Runnable的關(guān)聯(lián)
我們先看下Runnable的源碼:
public?interface?Runnable?{????
??public?abstract?void?run();
}
What? Runnable接口這么簡單?就一個run方法?
是的,你沒有看錯,Runnable接口就這么簡單。如果沒有Thread類,那么Runnable接口就是普通得不能再普通的一個接口了,我們在代碼中實現(xiàn)這個接口,也做不了任何事情!但由于得到Thread類的“青睞”,這個接口就變得不一般了!
那么Runnable接口得到Thread類的青睞,具體表現(xiàn)在哪呢?我們不妨先看看Thread類的定義
public?class?Thread?implements?Runnable{}
原來Thread是Runnable的一個實現(xiàn)類?。?!以程序人的第一直覺,那Thread自然應(yīng)該實現(xiàn)了run方法,我們繼續(xù)在源碼中搜尋!
@Override
public?void?run()?{????????
??if?(target?!=?null)?{
????????target.run();
??}
}
果不其然,Thread實現(xiàn)了run方法,并且有個判斷,當(dāng)target為null的時候什么也不做,否則執(zhí)行target的run方法,target又是什么呢?
private?Runnable?target;
target也是一個Runnable對象,那這個私有的字段在哪里賦值的呢?我們繼續(xù)尋找發(fā)現(xiàn)是在init方法里面進行賦值的,并且最終在構(gòu)造函數(shù)中調(diào)用了init方法,我們看看構(gòu)造函數(shù)的定義(Thread重載了多個構(gòu)造函數(shù))
public?Thread(Runnable?target)?{
????init(null,?target,?"Thread-"?+?nextThreadNum(),?0);
}
這里我們能看到Thread的構(gòu)造函數(shù)支持Runnable的參數(shù),不知道到目前大家有沒有捋清楚Thread與Runnable的關(guān)系,我們再總結(jié)一下:
1、Runnable原本平民,只是一個普通的接口。
2、Thread實現(xiàn)了Runnable接口,并且實現(xiàn)了接口的run方法。
3、Thread提供了重載的構(gòu)造函數(shù),接收Runnable類型的參數(shù)。在Thread重寫的run方法中對調(diào)用了構(gòu)造函數(shù)傳入的Runnable實現(xiàn)類的run方法。
所以不管我們用哪種方式創(chuàng)建線程,都要實現(xiàn)或者重寫run方法!并且這個run方法都是實現(xiàn)的Runnable接口中的方法。這個run方法就是我們自定義的需要在線程中處理的一些邏輯!那這個run方法在哪里調(diào)用的呢?直接調(diào)用run方法可以創(chuàng)建新的線程么?為什么我們在代碼中都是調(diào)用的start方法去啟動一個線程?start方法與run方法有什么關(guān)聯(lián)?
好奇心的驅(qū)使,我再次打開了源碼中的start方法一探究竟,發(fā)現(xiàn)這個方法中最主要的行為是調(diào)用了一個名為start0的方法。
public?synchronized?void?start()?{
??……
??start0();
??……
}
那start0又是什么呢?
private?native?void?start0();
看到native關(guān)鍵字我們就應(yīng)該知道,這個方法是JVM的方法,具體的實現(xiàn)需要查看C的代碼!
3、start方法與run方法的關(guān)聯(lián)
鑒于自己C語言很爛,且多年沒有碰過了,但是又想弄清楚start方法與run方法的關(guān)聯(lián),于是在網(wǎng)上查找了相關(guān)的資料,下面做一下整理。
推薦閱讀:多線程 start 和 run 方法的區(qū)別。
參考資料:
https://www.ibm.com/developerworks/cn/java/j-lo-processthread/
在Thread 類的頂部,有個native的registerNatives本地方法,該方法主要的作用就是注冊一些本地方法供 Thread 類使用,如start0(),stop0() 等等,可以說,所有操作本地線程的本地方法都是由它注冊的 . 這個方法放在一個static語句塊中,這就表明,當(dāng)該類被加載到JVM中的時候,它就會被調(diào)用,進而注冊相應(yīng)的本地方法。
private?static?native?void?registerNatives();????
??static?{
????registerNatives();
??}
??
}??
本地方法registerNatives是定義在Thread.c文件中的。T
hread.c是個很小的文件,定義了各個操作系統(tǒng)平臺都要用到的關(guān)于線程的公用數(shù)據(jù)和操作
JNIEXPORT?void?JNICALL
????Java_Java_lang_Thread_registerNatives?(JNIEnv?*env,?jclass?cls){
??????(*env)->RegisterNatives(env,?cls,?methods,?ARRAY_LENGTH(methods));
????}
????
????static?JNINativeMethod?methods[]?=?{
???????……
{"start0",?"()V",(void?*)&JVM_StartThread},
???????{"stop0",?"("?OBJ?")V",?(void?*)&JVM_StopThread},
??????……
};
到此,可以容易的看出Java線程調(diào)用start的方法,實際上會調(diào)用到JVM_StartThread方法,那這個方法又是怎樣的邏輯呢。實際上,我們需要的是(或者說 Java 表現(xiàn)行為)該方法最終要調(diào)用Java線程的run方法,事實的確如此。在jvm.cpp中,有如下代碼段:
JVM_ENTRY(void,?JVM_StartThread(JNIEnv*?env,?jobject?jthread))
??????……
?????native_thread?=?new?JavaThread(&thread_entry,?sz);
?????……
這里JVM_ENTRY是一個宏,用來定義JVM_StartThread 函數(shù),可以看到函數(shù)內(nèi)創(chuàng)建了真正的平臺相關(guān)的本地線程,其線程函數(shù)是 thread_entry,代碼如下所示。
static?void?thread_entry(JavaThread*?thread,?TRAPS)?{
??HandleMark?hm(THREAD);
???Handle?obj(THREAD,?thread->threadObj());
???JavaValue?result(T_VOID);
???JavaCalls::call_virtual(&result,obj,
???KlassHandle(THREAD,SystemDictionary::Thread_klass()),
???vmSymbolHandles::run_method_name(),
???vmSymbolHandles::void_method_signature(),THREAD);
}
可以看到調(diào)用了vmSymbolHandles::run_method_name方法,這是在 vmSymbols.hpp用宏定義的:
class?vmSymbolHandles:?AllStatic?{
??…?????????template(run_method_name,"run")
??…
}
至于run_method_name是如何聲明定義的,因為涉及到很繁瑣的代碼細節(jié),本文不做贅述。感興趣的讀者可以自行查看JVM的源代碼。

綜上所述,Java線程的創(chuàng)建調(diào)用過程如上圖所示,首先 , Java線程的start方法會創(chuàng)建一個本地線程(通過調(diào)用JVM_StartThread),該線程的線程函數(shù)是定義在jvm.cpp中的thread_entry,由其再進一步調(diào)用run方法。
可以看到Java線程的run方法和普通方法其實沒有本質(zhì)區(qū)別,直接調(diào)用run方法不會報錯,但是卻是在當(dāng)前線程執(zhí)行,而不會創(chuàng)建一個新的線程。關(guān)注公眾號Java技術(shù)棧在后臺回復(fù)面試獲取Java并發(fā)系列面試題及答案,我都整理好了。
二、FutureTask、Callable 與 Runnable
1、創(chuàng)建能獲取結(jié)果的異步線程
上面我們了解了創(chuàng)建線程創(chuàng)建的兩種方式,但是我們也能看到,通過runnable方式創(chuàng)建的線程無法獲取返回值,如果我們需要異步執(zhí)行某個操作,并且得到返回的結(jié)果,可能上面的兩種創(chuàng)建線程的方式就不適用啦(也不是說不可以,比如通過共享變量等方式,但是使用起來會比較麻煩)!在java中提供一種比較方便的方式,那就是使用FutureTask類,我們先看看使用方式:
public?class?MyFutrueTask?implements?Callable?{
????@Override
????public?String?call()?throws?Exception?{
????????System.out.println("我是Future模式的線程啦");
????????return?"Future模式的線程結(jié)束啦";
????}
????public?static?void?main(String[]?args)?throws?TimeoutException,?ExecutionException,?InterruptedException?{
????????FutureTask?futureTask?=?new?FutureTask(new?MyFutrueTask());
????????new?Thread(futureTask).start();
????????String?result?=?futureTask.get(2000,?TimeUnit.MILLISECONDS);
????????System.out.println(result);
????}
}
主類實現(xiàn)了一個名為Callable的接口,并且實現(xiàn)了接口的call方法,具體的業(yè)務(wù)邏輯就在call方法中實現(xiàn)!實現(xiàn)Callable接口的類的對象可以作為一個參數(shù)傳遞到創(chuàng)建的FutureTask對象的構(gòu)造函數(shù)中,而FutureTask類的對象又作為一個參數(shù)傳遞到Thread對象的構(gòu)造函數(shù)中……
根據(jù)上面我們對Thread和Runnable的了解,能夠作為參數(shù)傳入到Thread構(gòu)造函數(shù)的對象,一定是實現(xiàn)了Runnable接口的!那是不是說FutureTask對象就應(yīng)該是一個Runnable的實現(xiàn)類呢?
2、FutureTask實現(xiàn)
我們先看看FutureTask類的定義
public?class?FutureTask?implements?RunnableFuture
FutureTask是一個泛型類,并且實現(xiàn)了RunnableFutrue泛型接口,我們繼續(xù)跟進
public?interface?RunnableFuture?extends?Runnable,?Future?{???
????void?run();
}
RunnableFutrue接口繼承了Runnable接口,這也就是說FutureTask間接的實現(xiàn)了Runnable接口,F(xiàn)utureTask也是Runnable的一個實現(xiàn)類,那也就必須要實現(xiàn)run方法,還能夠?qū)嵗龑ο髠魅隩hread對象的構(gòu)造后函數(shù)!RunnableFutrue接口還繼承了另外的一個名為Futrue的泛型接口,我們看看該接口的定義
public?interface?Future?{
???boolean?cancel(boolean?mayInterruptIfRunning);
???boolean?isCancelled();
???boolean?isDone();
???V?get();
???V?get(long?timeout,?TimeUnit?unit);
}
根據(jù)這些方法的命名能夠看出來,F(xiàn)utureTask實現(xiàn)了Future接口后,就應(yīng)該擁有了取消線程、判斷線程運行狀態(tài)、獲取結(jié)果等功能!
我們整體看一下FutureTask的類圖:

FutureTask類中對這些接口的具體實現(xiàn)是怎么樣的呢?我們可以到FutureTask類中一探究竟,先瞅瞅構(gòu)造函數(shù)
public?interface?Future?{
???boolean?cancel(boolean?mayInterruptIfRunning);
???boolean?isCancelled();
???boolean?isDone();
???V?get();
???V?get(long?timeout,?TimeUnit?unit);
}
構(gòu)造函數(shù)接收一個Callable類型的參數(shù),Callable是一個泛型類型的接口,該接口只有一個名為call的方法。本來這也只是一個普通的接口,但由于收到FutrueTask的“青睞”,這個接口變得不一般了!
public?interface?Callable?{????
??V?call()?throws?Exception;
}
乍一看是不是覺得跟Runnable接口很像呢?但是Callable接口的call方法有返回值!Callable、Runnable、FutureTask之間是怎么關(guān)聯(lián)起來的呢?我們上面有說了FutureTask是Runnable的一個實現(xiàn)類,那FutureTask是不是應(yīng)該也實現(xiàn)了run方法呢?我們跟進一下代碼:
public?void?run()?{
????......
????try?{
????????Callable?c?=?callable;
????????if?(c?!=?null?&&?state?==?NEW)?{
????????????V?result;
????????????boolean?ran;
????????????try?{
????????????????result?=?c.call();
????????????????ran?=?true;
????????????}?catch?(Throwable?ex)?{
????????????????......
????????????}
????????????if?(ran)
????????????????set(result);
????????}
????}?finally?{
????????......
????}
}
Run方法的實現(xiàn)也比較簡單,上述代碼只保留了需要關(guān)注的代碼。
在run方法中調(diào)用了構(gòu)造函數(shù)傳入的Callable實現(xiàn)類的call方法,并且用result的變量接收方法的返回值,最后調(diào)用set方法將返回結(jié)果設(shè)置到類的屬性,由于FutureTask實現(xiàn)了Future接口,所以也就有了獲取返回值以及判斷線程是否運行完成、取消的能力!
也就是說,我們自定義Callable實現(xiàn)類的call方法,最終會在FutureTask類(也就是Runnable)的run方法中執(zhí)行!而Runnable中的run方法,最終又會在Thread類的run方法中執(zhí)行。






關(guān)注Java技術(shù)棧看更多干貨


