【IT168技術】多線程這個令人生畏的“洪水猛獸”,很多人談起多線程都心存畏懼。在Android開發過程中,多線程真的很難嗎?多線程程式的“麻煩”源于它很抽象、與單線程程式運作模式不同,但隻要掌握了它們的差別,編寫多線程程式就會很容易了。下面讓我們集中精力開始學習吧!
多線程案例——計時器
我在給我的學生講多線程的時候都會舉一個計時器的案例,因為計時器案例是多線程的經典應用。
這個案例中,螢幕啟動之後,進入如圖8-1所示的界面。
螢幕上有一個文本框用于顯示逝去的時間,此外還有一個“停止計時”按鈕。案例的用例圖如圖8-2所示。

▲圖8-1 計時器界面
▲圖8-2 計時器用例圖
能夠在螢幕上“實時地顯示”時間的流逝,單線程程式是無法實作的,必須要多線程程式才可以實作,即便有些計算機語言可以通過封裝好的類實作這一功能,但從本質上講這些封裝好的類就是封裝了一個線程。
綜上所述,完成本案例用到的知識及技術如下:
1)程序和線程的概念;
2)Java中的線程,在Java中建立線程的方式;
3)Android中的線程,包括:Message、Handler、Looper和HandlerThread等概念。
線程究竟是什麼?在Windows作業系統出現之前,個人計算機上的作業系統都是單任務系統,隻有在大型計算機上才具有多任務和分時設計。Windows、Linux作業系統的出現,把原本隻在大型計算機才具有的優點,帶到了個人計算機系統中。
程序概念
一般可以在同一時間内執行多個程式的作業系統都有程序的概念。一個程序就是一個執行中的程式,而每一個程序都有自己獨立的一塊記憶體空間、一組系統資源。在程序的概念中,每一個程序的内部資料和狀态都是完全獨立的。在Windows作業系統下我們可以通過〈Ctrl+Alt+Del〉組合鍵檢視程序,在UNIX和Linux作業系統下是通過PS指令檢視程序的。打開Windows目前運作的程序,如圖8-3所示。
▲圖8-3 Windows作業系統程序
在Windows作業系統中一個程序就是一個exe或dll程式,它們互相獨立,互相也可以通信,在Android作業系統中程序間的通信應用也是很多的。
線程概念
多線程指的是在單個程式中可以同時運作多個不同的線程,執行不同的任務。多線程意味着一個程式的多行語句可以看上去幾乎在同一時間内同時運作。
線程與程序相似,是一段完成某個特定功能的代碼,是程式中單個順序的流控制。但與程序不同的是,同類的多個線程共享一塊記憶體空間和一組系統資源,是以系統在各個線程之間切換時,資源占用要比程序小得多,正因如此,線程也被稱為輕量級程序。一個程序中可以包含多個線程。圖8-4所示是計時器程式程序和線程之間的關系,主線程負責管理子線程,即子線程的啟動、挂起、停止等操作。
▲圖8-4 程序和線程關系
Java中的線程
Java的線程類是java.lang.Thread類。當生成一個Thread類的對象之後,一個新的線程就産生了。Java中每個線程都是通過某個特定Thread對象的方法run()來完成其操作的,方法run( )稱為線程體。
下面是建構線程類幾種常用的方法:
public Thread()
public Thread(Runnable target)
public Thread(Runnable target, String name)
public Thread(String name)
參數target是一個實作Runnable接口的執行個體,它的作用是實作線程體的run()方法。目标target可為null,表示由本身執行個體來執行線程。name參數指定線程名字,但沒有指定的構造方法,線程的名字是JVM配置設定的,例如JVM指定為thread-1、thread-2等名字。
1、Java中的實作線程體方式1
在Java中有兩種方法實作線程體:一是繼承線程類Thread,二是實作接口Runnable。下面我們先看看繼承線程類Thread方式。
如果采用第1種方式,它繼承線程類Thread并重寫其中的方法 run(),在初始化這個類執行個體的時候,目标target可為null,表示由本執行個體來執行線程體。由于Java隻支援單重繼承,用這種方法定義的類不能再繼承其他父類,例如代碼清單8-1,完整代碼請參考chapter8_1工程中chapter8_1代碼部分。
【代碼清單8-1】
public class chapter8_1 extends Thread {
boolean isRunning = true ;
int timer = 0 ;
@Override
public void run() {
while (isRunning) {
try {
Thread.currentThread().sleep( 1000 );
timer ++ ;
System.out.println( " 逝去了 " + timer + " 秒 " );
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main( String [] args) {
chapter8_1 t1 = new chapter8_1();
t1.start();
System.out.println( " 計時器啟動... " );
BufferedReader br = new BufferedReader( new InputStreamReader(System.in));
try {
String line = br.readLine();
if (line.equalsIgnoreCase( " 1 " )) {
t1.isRunning = false ;
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
在main主方法中通過new chapter8_1()建立子線程,并通過t1.start()方法啟動子線程,main主方法所線上程為主線程,主線程負責管理其他的子線程。本例程序、主線程和子線程之間的關系如圖8-5所示。
子線程啟動之後就開始調用run()方法,run()是一個線程體,我們在子線程中處理事情就是在這裡編寫代碼實作的。本案例中子線程要做的事情就是:休眠1s,計時器加1,再反複執行。Thread.currentThread().sleep(1000)就是休眠1s。
為了能夠停止線程,我們在主線程中增加了一個辨別,通過在控制台輸入一個字元
“1”來改變該辨別t1.isRunning = false,進而結束這個線程。
▲圖8-5 線程間關系圖
注意:
事實上線程中有一個stop()方法也可以停止線程,但是由于這種方法會産生線程死鎖問題,是以在新版JDK中已經廢止了,它的替代解決方法就是增加辨別,就是我們在本例中采用的方案。
很多人覺得線程難了解,主要有兩個問題:
線程休眠,既然線程已經休眠了,程式的運作速度還能提高嗎?
線程體一般都進行死循環,既然線程死循環,程式就應該死掉了,就會沒有反應。
1.關于線程休眠問題
對線程休眠問題頭痛的讀者,其實還是在用單線程的思維模式考慮問題,多數情況下我們的PC都是單CPU的,某個時間點隻能有一個線程運作。所謂多線程就是多個線程交替執行就好像同時運作似的。是以,休眠目前線程可以交出CPU控制權,讓其他的線程有機會運作,多個線程之間隻有交替運作效率才是最高的,這就像我們開車過十字路口,隻有我等等,讓你先過,你再等等讓他先過,才能保證最高效率,否則就會造成交通系統崩潰,對線程情況也是一樣的。是以,多線程中線程的休眠是程式運作的最有效方式。
2.關于線程體死循環問題
在單線程中如果是死循環,程式應就會死掉,沒有反應,但是多線程中線程體(run方法)中的死循環,可以保證線程一直運作,如果不循環線程,則運作一次就停止了。在上面的例子中線程體運作死循環,可以保證線程一直運作,每次運作都休眠1s,然後喚醒,再然後把時間資訊輸出到控制台。是以,線程體死循環是保證子線程一直運作的前提。由于是子線程它不會堵塞主線程,就不會感覺到程式死掉了。但是需要注意的是有時我們确實執行一次線程體,就不需要循環了。
程式運作後開始啟動線程,線程啟動後就計算逝去的時間,每過1s将結果輸出到控制台。當輸入1字元後線程停止,程式終止。如圖8-6所示。
▲圖8-6 運作顯示圖
Java中的實作線程體方式2
上面介紹繼承Thread方式實作線程體,下面介紹另一種方式,這種方式是提供一個實作接口Runnable的類作為一個線程的目标對象,構造線程時有兩個帶有Runnable target參數的構造方法:
Thread(Runnable target);
Thread(Runnable target, String name)。
其中的target就是線程目标對象了,它是一個實作Runnable的類,在構造Thread類時候把目标對象(實作Runnable的類)傳遞給這個線程執行個體,由該目标對象(實作Runnable的類)提供線程體run()方法。這時候實作接口Runnable的類仍然可以繼承其他父類。
請參看代碼清單8-2,這是一個Java AWT的窗體應用程式,完整代碼請參考chapter8_2工程中chapter8_2_1代碼部分。
【代碼清單8-2】
public class chapter8_2_1 extends Frame implements ActionListener, Runnable {
private Label label;
private Button button1;
private Thread clockThread;
private boolean isRunning = false ;
private int timer = 0 ;
public chapter8_2_1() {
button1 = new Button( " 結束計時 " );
label = new Label( " 計時器啟動... " );
button1.addActionListener(this);
setLayout( new BorderLayout());
add(button1, " North " );
add(label, " Center " );
setSize( 320 , 480 );
setVisible( true );
clockThread = new Thread(this);
clockThread.start();
isRunning = true ;
}
@Override
public void actionPerformed(ActionEvent event) {
isRunning = false ;
}
@Override
public void run() {
while (isRunning) {
try {
Thread.currentThread().sleep( 1000 );
timer ++ ;
label.setText( " 逝去了 " + timer + " 秒 " );
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main( String args[]) {
chapter8_2_1 a = new chapter8_2_1();
}
}
其中關于Java AWT知識本書就不在這裡介紹了,有興趣的讀者可以自己看看相關書籍。在本例中建構AWT窗體的應用程式方式是繼承Frame類。采用第1種方式——繼承方式實作線程體是不可以的,因為Java是單繼承的,這個類不能既繼承Frame又繼承Thread。應該采用第2種方式——實作Runnable接口方式。Runnable接口也有一個run()方法,它是實作線程體方法,其代碼處理與上一節是一樣。需要注意的是,在第2種方法中,建立了一個Thread成員變量clockThread,才用構造方法new Thread(this)建立一個線程對象,其中建立線程使用的構造方法是Thread(Runnable target),其中的this就是代表本執行個體,它是一個實作了Runnable接口的實作類。
程式運作結果如圖8-7所示,螢幕開始加載的時候線程啟動開始計算時間,1s更新一次UI,當單擊“結束計時”按鈕時,停止計時。
▲圖8-7 運作結果圖
Java中的實作線程體方式3
實作線程體方式3是實作線程體方式2的變種,本質上還是實作線程體方式2,但是在Android應用開發中經常采用第3種方式。下面我們看第3種方式的計時器代碼清單8-3,完整代碼請參考chapter8_2工程中 chapter8_2_2代碼部分。
【代碼清單8-3】
public class chapter8_2_2 extends Frame implements ActionListener {
private Label label;
private Button button1;
private Thread clockThread;
private boolean isRunning = false ;
private int timer = 0 ;
public chapter8_2_2() {
button1 = new Button( " 結束計時 " );
label = new Label( " 計時器啟動... " );
button1.addActionListener(this);
setLayout( new BorderLayout());
add(button1, " North " );
add(label, " Center " );
setSize( 320 , 480 );
setVisible( true );
clockThread = new Thread( new Runnable() {
@Override
public void run() {
while (isRunning) {
try {
Thread.currentThread().sleep( 1000 );
timer ++ ;
label.setText( " 逝去了 " + timer + " 秒 " );
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
clockThread.start();
isRunning = true ;
}
@Override
public void actionPerformed(ActionEvent event) {
isRunning = false ;
}
public static void main( String args[]) {
chapter8_2_2 a = new chapter8_2_2();
}
}
與第2種方式比較,我們發現Frame類不再實作Runnable接口了,而是在執行個體化Thread類的時候,定義了一個實作Runnable接口的匿名内部類:
clockThread = new Thread( new Runnable() {
@Override
public void run() {
while (isRunning) {
try {
Thread.currentThread().sleep( 1000 );
timer ++ ;
label.setText( " 逝去了 " + timer + " 秒 " );
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
有關Java多線程的内容還有很多,例如線程優先級、線程同步等,由于這些内容與本書關系不是很緊密,是以不再介紹了,有關其他的線程知識可以參考Java方面的書籍。接下來介紹一下Android中的線程。
Android中的線程
在Android平台中多線程應用很廣泛,在UI更新、遊戲開發和耗時處理(網絡通信等)等方面都需要多線程。Android線程涉及的技術有:Handler;Message;MessageQueue;Looper;HandlerThread。
Android線程應用中的問題與分析
為了介紹這些概念,我們把計時器的案例移植到Android系統上,按照在Frame方式修改之後的代碼清單8-4,完整代碼請參考chapter8_3工程中 chapter8_3代碼部分。
【代碼清單8-4】
public class chapter8_3 extends Activity {
private String TAG = " chapter8_3 " ;
private Button btnEnd;
private TextView labelTimer;
private Thread clockThread;
private boolean isRunning = true ;
private int timer = 0 ;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
btnEnd = (Button) findViewById(R.id.btnEnd);
btnEnd.setOnClickListener( new OnClickListener() {
@Override
public void onClick(View v) {
isRunning = false ;
}
});
labelTimer = (TextView) findViewById(R.id.labelTimer);
clockThread = new Thread( new Runnable() {
@Override
public void run() {
while (isRunning) {
try {
Thread.currentThread().sleep( 1000 );
timer ++ ;
labelTimer.setText( " 逝去了 " + timer + " 秒 " );
Log .d(TAG, " lost time " + timer );
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
clockThread.start();
}
}
程式打包運作結果出現了異常,如圖8-8所示。
▲圖8-8 運作結果異常圖
我們打開LogCat視窗,出錯日志資訊如圖8-9所示。
▲圖8-9 出錯日志
系統抛出的異常資訊是“Only the original thread that created a view hierarchy can touch its views”,在Android中更新UI處理必須由建立它的線程更新,而不能在其他線程中更新。上面的錯誤原因就在于此。
現在分析一下上面的案例,在上面的程式中有兩個線程:一個主線程和一個子線程,它們的職責如圖8-10所示。
由于labelTimer是一個UI控件,它是在主線程中建立的,但是它卻在子線程中被更新了,更新操作在clockThread線程的run()方法中實作,代碼如下:
▲圖8-10 線程職責
clockThread = new Thread( new Runnable() {
@Override
public void run() {
while (isRunning) {
try {
Thread.currentThread().sleep( 1000 );
timer ++ ;
labelTimer.setText( " 逝去了 " + timer + " 秒 " );
Log .d(TAG, " lost time " + timer );
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
這樣的處理違背了Android多線程程式設計規則,系統會抛出異常“Only the original thread that created a view hierarchy can touch its views”。
要解決這個問題,就要明确主線程和子線程的職責。主線程的職責是建立、顯示和更新UI控件、處理UI事件、啟動子線程、停止子線程;子線程的職責是計算逝去的時間和向主線程發出更新UI消息,而不是直接更新UI。它們的職責如圖8-11所示。
▲圖8-11 線程職責
主線程的職責是顯示UI控件、處理UI事件、啟動子線程、停止子線程和更新UI,子線程的職責是計算逝去的時間和向主線程發出更新UI消息。但是新的問題又出現了:子線程和主線程如何發送消息、如何通信呢?
在Android中,線程有兩個對象—消息(Message)和消息隊列(MessageQueue)可以實作線程間的通信。下面再看看修改之後的代碼清單8-5,完整代碼請參考chapter8_4工程中chapter8_4代碼部分。
【代碼清單8-5】
public class chapter8_4 extends Activity {
private String TAG = " chapter8_3 " ;
private Button btnEnd;
private TextView labelTimer;
private Thread clockThread;
private boolean isRunning = true ;
private Handler handler;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
btnEnd = (Button) findViewById(R.id.btnEnd);
btnEnd.setOnClickListener( new OnClickListener() {
@Override
public void onClick(View v) {
isRunning = false ;
}
});
handler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case 0 :
labelTimer.setText( " 逝去了 " + msg.obj + " 秒 " );
}
}
};
labelTimer = (TextView) findViewById(R.id.labelTimer);
clockThread = new Thread( new Runnable() {
@Override
public void run() {
int timer = 0 ;
while (isRunning) {
try {
Thread.currentThread().sleep( 1000 );
timer ++ ;
Message msg = new Message();
msg.obj = timer ;
msg.what = 0 ;
handler.sendMessage(msg);
Log .d(TAG, " lost time " + timer );
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
clockThread.start();
}
有的時候為了将Android代碼變得更加緊湊,把線程的建立和啟動編寫在一條語句中,如下面chapter8_5的代碼片段。代碼清單8-6所示,完整代碼請參考chapter8_5工程中 chapter8_5代碼部分。
【代碼清單8-6】
new Thread() {
@Override
public void run() {
int timer = 0 ;
while (isRunning) {
ry {
Thread.currentThread().sleep( 1000 );
timer ++ ;
/ labelTimer.setText( " 逝去了 " + timer + " 秒 " );
Message msg = new Message();
msg.obj = timer ;
msg.what = 0 ;
handler.sendMessage(msg);
Log .d(TAG, " lost time " + timer );
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
chapter8_5代碼看起來有些糊塗吧?chapter8_4和chapter8_5建立線程的差別是:chapter8_4采用Thread(Runnable target)構造方法建立一個線程,需要提供一個Runnable接口對象,需要提供的參數是實作了Runnable接口的匿名内部類對象。chapter8_5采用Thread()構造方法建立一個線程,在這裡采用了簡便的程式設計方法,直接建立一個Thread類,同時重寫run()方法。
chapter8_5程式設計方法雖然晦澀難懂,而且違背了Java程式設計規範,程式結構也比較混亂,但卻是Android習慣寫法,這主要源于Android對于減少位元組碼的追求。究竟這兩種方式在性能上有多少差别呢?誠實地講我沒有做過測試和求證,在我看來就上面的程式而言它們之間不會有太大差别,由于本書要盡可能遵守Java程式設計規範和Android的程式設計習慣,是以本書中兩種程式設計方式都會采用,如果給大家帶來不便敬請諒解。
運作模拟器結果如圖8-1所示,加載螢幕後馬上開始計時,也可以單擊“停止計時”按鈕來停止計時。