有这样一道面试题。

面试官:“创建线程有哪几种方式?”;

面试者:“四种,第一是继承Thread类,第二是实现Runnable接口,第三是实现Callable接口创建具有返回值的线程,最后是通过线程池来创建线程。”

啊,多么标准的答案!

到底有几种方式呢?

这里不是想争辩说没有这四种,而是想谈谈我的一些理解。

首先我们来看java.lang.Thread类的类注释,有这样一段英文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
* There are two ways to create a new thread of execution. One is to
* declare a class to be a subclass of <code>Thread</code>. This
* subclass should override the <code>run</code> method of class
* <code>Thread</code>. An instance of the subclass can then be
* allocated and started.

......

* The other way to create a thread is to declare a class that
* implements the <code>Runnable</code> interface. That class then
* implements the <code>run</code> method. An instance of the class can
* then be allocated, passed as an argument when creating
* <code>Thread</code>, and started.
* <p>

很明显,JDK的注释说明:There are two ways to create a new thread of execution. One is to....The other way to create ...。有两种方式可以创建新的执行线程,一种是继承Thread类,另一种是实现Runnable接口。

怎么去理解这个东西?我们先来看Thread类和Runnable接口的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Thread implements Runnable {
......

private Runnable target;

@Override
public void run() {
if (target != null) {
target.run();
}
}

......
}

@FunctionalInterface
public interface Runnable {
public abstract void run();
}

Runnable接口中的run方法抽象的是“可做的事情”,可将其称为任务。

Thread类是对线程进行建模,一个Thread类的实例对象就是一个线程,线程启动后有自己的任务,用Runnable成员变量存储。

Thread类实现了Runnable接口的run方法,用来驱动成员变量Runnable存储的任务的执行。线程start之后会自动调用Thread类的run方法。

所以问题不应该是创建线程有几种方式,而是任务的存储和驱动有几种方式。

第一种:继承Thread

不使用Runnable成员变量存储,直接将任务告诉Thread类的run方法。

1
2
3
4
5
6
7
8
9
10
class MyExtendsThread extends Thread {
@Override
public void run() {
System.out.println("extend thread to create a thread.");
}
}

public static void main(String[] args) {
new MyExtendsThread().start();
}

我们无法去修改Thread类的run方法,只能进行扩展增强(继承的方式):继承Thread类后对run方法进行重写覆盖,将要执行的任务写在子类的run方法中。

这种方式没有用Thread类的成员变量Runnable存储任务,破坏了Thread类对线程的建模思想。

第二种:实现Runnable接口

将任务包装成Runnable对象,通过Thread类的有参构造器将任务传递给其Runnable成员变量进行存储。

1
2
3
4
5
6
7
8
9
10
class MyImplRunnableThread implements Runnable {
@Override
public void run() {
System.out.println("implements Runnable to create a thread.");
}
}

public static void main(String[] args) {
new Thread(new MyImplRunnableThread()).start();
}

所以,创建线程有几种方式这个问题,我给出的答案是一种:创建Thread类的对象。

不同的是如何去存储和驱动线程要执行的任务。

第三种:实现Callable接口

我们想在线程执行完任务之后具有返回值。

按照Thread类对线程的建模,我们需要将待执行的任务包装成一个Runnable对象交给Thread类的成员变量进行存储。

任务的驱动是由Thread类的run方法来做的,但这个方法没有返回值,我们也不能去直接调用这个run方法,否则就不是创建一个线程了。

所以我们应该在这个run方法中将返回值保存在某个地方,在将来的某个时刻再去获取这个返回值,但任务何时执行完我们无法得知,我们也不一定立即需要这个返回值,所以可以使用j.u.c包中的Future类,但它不是一个Runnable,于是我们有RunnableFuture这个组合接口,并且在j.u.c包下有一个实现类FutureTask。我们使用Callable接口抽象出具有返回值的事情,交给FutureTask的成员变量进行存储。因为FutureTask是一个Runnable,符合Thread类对线程的建模,所以可以正常使用Thread类的run方法进行驱动,同时FutureTask是一个Future对象,我们可以获取到线程执行完Callable之后的返回值。简单地看下FutureTask类的run方法的具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public void run() {
if (state != NEW ||
!UNSAFE.compareAndSwapObject(this, runnerOffset,
null, Thread.currentThread()))
return;
try {
Callable<V> c = callable;
if (c != null && state == NEW) {
V result;
boolean ran;
try {
// 调用得到结果
result = c.call();
ran = true;
} catch (Throwable ex) {
result = null;
ran = false;
setException(ex);
}
if (ran)
// 将结果进行存储
set(result);
}
} finally {
// runner must be non-null until state is settled to
// prevent concurrent calls to run()
runner = null;
// state must be re-read after nulling runner to prevent
// leaked interrupts
int s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}

/** The result to return or exception to throw from get() */
private Object outcome; // non-volatile, protected by state reads/writes

protected void set(V v) {
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
// 存储到成员变量outcome中
outcome = v;
UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
finishCompletion();
}
}

主要逻辑很简单:获取成员变量Callable并调用其call方法执行任务得到返回值,然后存储到成员变量outcome中。所以取值的时候只需取这个outcome成员变量即可。

于是,就有了所谓的第三种创建线程的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static class MyCallableThread implements Callable<String> {
@Override
public String call() throws Exception {
return "implements Callable<V> to create a thread";
}
}

public static void main(String[] args) {
MyCallableThread callableThread = new MyCallableThread();
FutureTask<String> ft = new FutureTask<>(callableThread);
new Thread(ft).start();
try {
String s = ft.get();
System.out.println(s);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}

实际上还是创建了一个Thread类对象,只是任务存储和驱动的方式变了。

第四种:使用线程池创建

先来看下如何使用线程池:

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(2);
executorService.execute(new MyImplRunnableThread());
Future<String> submit = executorService.submit(callableThread);
try {
String s = submit.get();
System.out.println(s);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}

通过JDK提供的工具类Executors的静态方法newFixedThreadPool创建固定线程数量的线程池,可以用线程池来执行Runnable任务,也可以往线程池中提交具有返回值的任务,会返回一个Future对象用来获取返回值。

我们知道线程池底层是由线程工厂来创建线程的,默认的线程工厂实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
static class DefaultThreadFactory implements ThreadFactory {
private static final AtomicInteger poolNumber = new AtomicInteger(1);
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;

DefaultThreadFactory() {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
namePrefix = "pool-" +
poolNumber.getAndIncrement() +
"-thread-";
}

public Thread newThread(Runnable r) {
Thread t = new Thread(group, r,
namePrefix + threadNumber.getAndIncrement(),
0);
if (t.isDaemon())
t.setDaemon(false);
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
}

实际上创建线程还是去创建Thread类的对象。线程池也只是改变了任务存储和驱动的方式。

第五种:使用定时器Timer

JDK定时器Timer可以创建定时执行任务的线程,使用方式如下:

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("use Timer to create a thread.");
}
}, 1L);
}

Timer类中,有以下成员变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Timer {
....
private final TaskQueue queue = new TaskQueue();
/**
* The timer thread.
*/
private final TimerThread thread = new TimerThread(queue);

....

class TimerThread extends Thread {...}

....
}

可以看到在创建Timer对象时,就已经初始化好了TimerThread类的对象,这个TimerThread类继承自Thread类,所以TimerThread类的run方法是任务驱动的方式。TimerTask类实现了Runnable接口,所以它是用来包装任务的,schedule方法用来提交任务至TaskQueue队列进行存储。

总结

至此,我们可以这样理解,本质上创建线程只有一种方式:创建Thread类的对象。其余方式均是在此之上的封装,改变了任务存储和驱动的方式而已。

那我们到底该选择那种任务存储和驱动的方式呢?

按照Thread类对线程的建模,对于一般的任务而言,我们应该将任务包装成Runnable或其子类对象,通过Thread类的有参构造器用Runnable成员变量进行存储,通过Thread类的run方法驱动任务的执行。