1. 程式人生 > >Java多執行緒,Thread和Runnable究竟該用哪個

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。