Java多執行緒,Thread和Runnable究竟該用哪個
很久沒寫部落格了,內心有一絲罪惡感。其中一個原因是最近做的一些東西不適合在部落格上公開。
今天抽空來說說Java多執行緒中的一個小話題,也是新人經常會遇到的。起因是我在給新人找培訓資料的時候,在網上看到了很多Thread和Runnable究竟該用哪個的討論,絕大多數這類文章都是同一個祖師爺的,都用了視窗賣票的例子。以下地址是一篇比較有代表性的(搞笑的是連Runnable都拼錯了):
http://www.oschina.net/question/565065_86563
可見新人們在網上找到的資料都是些這種東西,真讓人憂慮啊。這篇博文槽點太多,我就不一一噴了,我們來把這篇認為正確的“最終寫法”拿出來實際編一編試試(我做了一處小改動,因為原始寫法執行緒還有死迴圈問題,另外把票數從100張改到了10張,便於分析)。
class ThreadTest implements Runnable { private int tickets = 10; public void run() { while (true) { if (tickets > 0) { System.out.println(Thread.currentThread().getName() + " is saling ticket " + tickets--); } else { break; } } } } public class ThreadDemo1 { public static void main(String[] args) { ThreadTest t = new ThreadTest(); new Thread(t).start(); new Thread(t).start(); new Thread(t).start(); } }
初看執行結果是正常的,3個視窗賣10張票。可是多執行幾次問題就來了,可能我臉比較黑,只跑了4遍就遇到了一次這樣的結果:
Thread-0 is saling ticket 10
Thread-0 is saling ticket 8
Thread-2 is saling ticket 9
Thread-2 is saling ticket 6
Thread-1 is saling ticket 10
Thread-1 is saling ticket 4
Thread-2 is saling ticket 5
Thread-2 is saling ticket 2
Thread-2 is saling ticket 1
Thread-0 is saling ticket 7
Thread-1 is saling ticket 3
是的,編號10的票被賣了2次。為什麼會這樣,相信對多執行緒有了解的程式設計師都應該知道,--操作符內部的實現並不是原子的。解決方法很簡單,一是用synchronized這種內建鎖,二是用AtomicInteger這樣的concurrent包裡封裝好的元素,簡潔起見我用第二種實現如下:
import java.util.concurrent.atomic.AtomicInteger;
//public class MyThread extends Thread { //兩種寫法一樣
public class MyThread implements Runnable{
private AtomicInteger tickets = new AtomicInteger(10);
@Override
public void run() {
while (true) {
int ticketnum;
if ((ticketnum = tickets.getAndDecrement()) > 0) {
System.out.println(Thread.currentThread().getName() + " is saling ticket: "
+ ticketnum);
} else {
break;
}
}
}
public static void main(String[] args) {
MyThread mt = new MyThread();
Thread t1 = new Thread(mt, "Win1");
Thread t2 = new Thread(mt, "Win2");
Thread t3 = new Thread(mt, "Win3");
t1.start();
t2.start();
t3.start();
}
}
以上程式碼還說明一個問題,這類博文中說的“Runnable可以用來在多執行緒間共享物件,而Thread不能共享物件”,純屬無稽之談,Thread本身就實現了Runnable,不會導致能不能共享物件這種區別。兩者最大(甚至可以說唯一,因為其他差異對新人來說無關緊要)的區別是Thread是類而Runnable是介面,至於用類還是用介面,取決於繼承上的實際需要。
最後回答標題的問題:Thread和Runnable究竟該用哪個?我的建議是都不用。因為Java這門語言發展到今天,在語言層面提供的多執行緒機制已經比較豐富且高階,完全不用線上程層面操作。直接使用Thread和Runnable這樣的“裸執行緒”元素比較容易出錯,還需要額外關注執行緒數等問題。建議:
簡單的多執行緒程式,使用Executor。
簡單的多執行緒,但不想關注執行緒層面因素,又熟悉Java8的:使用Java8的並行流,它底層基於ForkJoinPool,還能享受函數語言程式設計的快捷。
複雜的多執行緒程式,使用一個Actor庫,首推Akka。