在進行多線程編程中,比較重要也是比較困難的一個操作就是如何獲取線程中的信息。大多數人會采取比較常見的一種方法就是將線程中要返回的結果存儲在一個字段中,然後再提供一個獲取方法將這個字段的內容返回給該方法的調用者。如以下的ReturnThreadInfo類:
package threadtest1;
/**
*
* @author shi mingxiang
*/
public class ReturnThreadInfo extends Thread {
private String str;
public ReturnThreadInfo() {
this.str = "Hello";
}
public void run(){
this.str = "Hello World!";
}
public String getThreadInfo(){
return this.str;
}
}
大家可以看到該類是一個線程類並含有一個初始值為"Hello"的字段str以及一個可以返回str值的方法:getThreadInfo(),而且當這個線程啟動後str會被賦於新值:"Hello World!"。現在我想在另外一個類中啟動ReturnThreadInfo線程,並通過getThreadInfo()方法獲取值為"Hello World!"的變量並打印輸出到控制台中。以下給出一個實現該功能的Main類:
package threadtest1;
/**
*
* @author shi mingxiang
*/
public class Main{
public Main() {
}
/**
* @param args the command line arguments
*/
public static void main(String[] args) {
ReturnThreadInfo returnThreadInfo = new ReturnThreadInfo();
returnThreadInfo.start(); //創建並啟動ReturnThreadInfo線程
System.out.println(returnThreadInfo.getThreadInfo()); //獲取並輸出returnThreadInfo對象的str的值
}
}
以上是一個多數熟悉單線程編程的人在第一反應下給出的實現方法。但是該類在運行的時候輸出的結果卻不是期望的"Hello World!"而是"Hello",這是由於線程的競爭條件導致的(由於ReturnThreadInfo線程和Main線程的優先級都為5,所以在很大幾率上ReturnThreadInfo線程的run()方法還沒有運行,Main類就已經運行System.out.println(returnThreadInfo.getThreadInfo());將"Hello"輸出了。具體的原理可以參見另一篇文章:"Java多線程的幾點誤區")。有的人可能會立即想到把ReturnThreadInfo線程的優先級設高些(比如最大的10)就可以returnThreadInfo線程的run()方法先運行完,然後Main類的System.out.println(returnThreadInfo.getThreadInfo())再運行,這樣輸出的結就一定是期望的"Hello World!"了。這種通過調整線程優先級的方法固然可以在某種程度上解決該問題,但是線程爭用CPU運行時間的原理卻決不僅僅只是優先級高低的原因(優先級高的線程並不意味著一定比優先級低的線程先運行,只是幾率要更大一些)。你並不希望ReturnThreadInfo線程9999次都比Main先運行,卻在最關鍵的一次在Main之後再運行。因此下面給出兩種比較常見的獲取線程信息的方法:
一、輪詢
比較常見的一種解決方案是,讓線程類獲取方法在結果字段設置之前返回一個標志值。然後主線程定時詢問獲取方法,看是否返回了標志之外的值。以下給出了具體的實現方法,該方法不斷測試str的值是否為"Hello",如果不為"Hello"才打印輸出它。例如:
package threadtest1;
/**
*
* @author shi mingxiang
*/
public class Main{
public Main() {
}
/**
* @param args the command line arguments
*/
public static void main(String[] args) {
ReturnThreadInfo returnThreadInfo = new ReturnThreadInfo();
returnThreadInfo.start(); //創建並啟動ReturnThreadInfo線程
while(true){
String str = returnThreadInfo.getThreadInfo();
if(!str.equals("Hello")){
System.out.println(returnThreadInfo.getThreadInfo());
break;
}
}
}
}
這種方案雖然能起到作用,但是它做了大量不需要做的工作。事實上,還有一種更簡單有效的方法來解決這個問題。
二、回調
輪詢方法最大的特點是主類Main不斷詢問線程類是否結束,這實際上大量浪費了運行時間,特別是當線程特別多的時候。因此如果反過來在線程結束時,由線程自己告訴主類Main線程已經結束,然後Main再獲取並輸出str的值,這樣就避免了輪詢方法所帶來的不必要的系統開銷問題。
在具體的實現過程中,線程可以在結束時通過調用主類中的一個方法來實現告知功能,這種方法叫做回調。這樣主類Main就可以在等待線程結束時休息,也就不會占用運行線程的時間。下面是修改後的Main類:
public class Main{
public Main() {
}
/**
* @param args the command line arguments
*/
public static void main(String[] args) {
// TODO code application logic here
ReturnThreadInfo returnThreadInfo = new ReturnThreadInfo();
returnThreadInfo.start();
}
public static void receiveStr(String str){
System.out.println(str);
}
}
相比於前面,我們在Main類中添加了一個靜態方法receiveStr(String str),該方法是供線程結束之前調用,通過參數str將要返回的線程信息返回給Main類並輸出顯示出來。下面是修改後的ReturnThreadInfo類,該類在線程結束前回調了Main.receiveStr方法,通知線程已結束。
package threadtest1;
/**
*
* @author shi mingxiang
*/
public class ReturnThreadInfo extends Thread {
private String str;
public ReturnThreadInfo() {
this.str = "Hello";
}
public void run(){
this.str = "Hello World!";
Main.receiveStr(str); //回調receiveStr方法
}
}
如果有很多個對象關心線程的返回的信息,線程可以保存一個回調對象列表。某個對象可以通過已經定義的一個對象將自己添加到列表中,表示自己對這些信息的關注。如果有多個類的實例關心這些信息,也可以定義一個interface,在interface中聲名回調方法,然後這些類都實現這個接口。其實這是典型的Java處理事件的方法,這麼做可以使得回調更靈活,可以處理涉及更多線程、對象和類的情況。稍後會給出這種模仿事件處理模型的回調的實現方法。