很久沒寫部落格了,內心有一絲罪惡感。其中一個原因是最近做的一些東西不適合在部落格上公開。
今天抽空來說說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。