很久没写博客了,内心有一丝罪恶感。其中一个原因是最近做的一些东西不适合在博客上公开。
今天抽空来说说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。