创建线程的方式
为什么说本质上只有1种创建线程的方式?
创建线程的方式有几种?答案可能不尽相同,常见的有2种、4种等等。
其实不然,我们在使用多线程时,常见的至少有4种方式。但其创建线程的方式,有且只有1种,即通过构造一个Thread类来创建线程。
注意:在面试时,我们可以说使用多线程的方式常见的有4种,但它们的本质都是通过构造Thread类创建线程。
四种常见的使用方式
常见的4种使用方式如下:
- 继承Thread
- 实现Runnable,传给Thread作为构造参数
- 实现Runnable或者Callable,传递给线程池
- 实现Callable,构建FutureTask传给Thread
接下来,我们来分析这4种方法,来解释为什么本质上只有1种创建线程的方式。
先来看最基础的前两种方式:
继承Thread和实现Runnable的方式的本质
继承Thread
public class MyThread extends Thread {
@Override
public void run() {
//TODO ...
}
}
实现Runnable
public class MyRunableImpl implements Runnable {
@Override
public void run() {
//TODO ...
}
}
创建线程并运行
public static void main(String[] args) {
Thread t1 = new MyThread();
t1.start();
Thread t2 = new Thread(new MyRunableImpl());
t2.start();
}
我们都知道,Thread#start方法运行后,经过一些操作会最终调用run()执行我们的操作,那么这两种方式是如何被调用到run()的呢?我们来查看Thread#run()。
public class Thread implements Runnable {
private Runnable target;
public Thread(Runnable target) {
// 调用init方法,最终在init的重构方法中赋值给target成员变量,即this.target = target
init(null, target, "Thread-" + nextThreadNum(), 0);
}
@Override
public void run() {
if (target != null) {
target.run();
}
}
}
通过代码可以清楚的看到:
- 当Runnable的成员变量不为空时,会通过构造函数传入Runnable实现类,new Thread后,就会执行实现类的run方法。
- 当我们继承Thread类的,实际上是重写了Thread类中的run方法,new Thread后,run方法被调用时就执行了我们的逻辑。
但总的来说,这两种方式其实是一样的,都是通过构建Thread类创建线程。
线程池创建的本质
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;
}
}
这是很多线程池的默认线程工厂,当线程数小于核心线程时,newThread方法将被调用,从 newThread方法可以看出,将Runnable实现类的参数传给Thread作为构造参数,不过多了很多其他参数,但其实就是第2种。
实现Callable,构建FutureTask的本质
翻看源码,我们就知道FutureTask实现了Runnable接口,因此,作为一个Runnable接口的子类,传递给Thread作为构造参数,其实也是第2种。
综述,我们虽然发现了4种不同方式,但他们的原理都是一样的,即new Thread()或者new Thread(Runnable r)的方式,都是通过new一个Thread实例。
继承Thread、实现Runnable和Callable的区别
到这里,我们已经知道创建线程只有1种方式,但使用形式常见的有4种。但这4种又有共同点,归纳一下,我们可以分为3类:
- 继承Thread
- 实现Runnable
- 实现Callable
那它们之间有什么区别呢?
实现Runnable接口比继承Thread好在哪里?
主要有3点:
- Java只支持单继承,所以继承Thread以后不能继承其他类
- 实现Runnable,该类作为1个任务类。Thread作为线程类,达到解耦的目的。
- 实现Runnable后,可以被多个线程共享并执行。
有了Runnable,为什么还需要Callable?
// Runnable
public interface Runnable {
void run();
}
// Callable
public interface Callable<V> {
V call() throws Exception;
}
通过上面的接口定义我们不难看出Runnable和Callable的区别:
- Runnable没有返回值,Callable使用泛型返回
- Runnable方法不能向上抛出异常,但Callable可以向上抛出异常

当我们实现了Runnable方法,就不能向上抛出异常,只能使用try/catch包裹。
**那为什么Runnable要设计成不支持向上抛出异常呢?**主要是调用Runnable#run方法的类(比如Thread类和线程池)是Java直接提供的,不是我们编写,所以即使抛出异常,我们也不能捕获它们。
启动前的准备,创建线程
以无参构造函数为例,new Thread()只是调用了init(),该方法实际代码比较长,我挑了比较关键的出来。
private volatile int threadStatus = 0;
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
this.name = name;
// 设置父线程
Thread parent = currentThread();
this.group = g;
// 继承父线程的守护属性
this.daemon = parent.isDaemon();
// 继承父线程的优先级
this.priority = parent.getPriority();
// classloader
if (security == null || isCCLOverridden(parent.getClass()))
this.contextClassLoader = parent.getContextClassLoader();
else
this.contextClassLoader = parent.contextClassLoader;
this.target = target;
setPriority(priority);
// 设置 父线程的 inheritThreadLocals
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
// 设置栈空间
this.stackSize = stackSize;
tid = nextThreadID();
}
从代码中可以看出,new Thread()只是设置了线程相关属性,并没有向底层或操作系统申请线程。设置的属性包括父线程,父线程的守护属性、优先级、inheritThreadLocals以及栈空间等.这里还需要注意的是,当构造函数传入Runnable参数时,会将该值赋值给target变量,这是run()能执行创建线程时传入Runnable#run()的关键。
下一节我们会聊到Java的线程状态。代码的第一行,threadStatus变量默认被赋值为0,它代表了线程当前线程的状态为 NEW ,NEW 就是最开始的状态,表示线程被设置创建完成,但还没有启动(即还没有到 RUNNABLE )。
通过threadStatus获取线程状态的代码如下,了解即可。
public static State toThreadState(int var0) {
if ((var0 & 4) != 0) {
return State.RUNNABLE;
} else if ((var0 & 1024) != 0) {
return State.BLOCKED;
} else if ((var0 & 16) != 0) {
return State.WAITING;
} else if ((var0 & 32) != 0) {
return State.TIMED_WAITING;
} else if ((var0 & 2) != 0) {
return State.TERMINATED;
} else {
return (var0 & 1) == 0 ? State.NEW : State.RUNNABLE;
}
}
启动线程,start() & run()
对于Java程序员,我们只需要将线程需要运行的代码放到run()中,然后调用start()启动线程。Java会帮我们执行start()逻辑启动线程,start()中会调用run()方法来执行我们的代码。用起来就很方便了,那我们来看看这两个方法是如何配合完成我们的任务。
start()
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
start()方法主要干了这几件事:
- 判断threadStatus,在创建线程的时候说过,创建好线程时默认为0,因此才可以成功被调用,这也是为什么start()不能被重复执行的原因(面试题)。
- 添加该线程到线程组
- 调用start0()本地方法
start()执行完成过程中,会将threadStatus变为RUNNABLE,这是第2个Java线程状态,下一节将做详细介绍。
关于start0()本地方法的源码就不展开讲了,有兴趣的小伙伴推荐阅读这篇文章从 Java 到 C++, 以 JVM 的角度看 Java 线程的创建与运行 - Android - 掘金
该方法主要做的事情包括:
- 在VM中运行JVM_StartThread方法,创建一个JavaThread C对象。
- 在JavaThread对象创建过程中,创建一个OSThread对象,且JavaThread对象保持对OSThread对象的引用(作用是方便调用管理)。
- 在OSTread创建过程中,会创建一个平台相关的底层级线程,如果这个底层级线程创建失败,那么抛出异常。
- 底层级线程开始运行,并执行java.lang.Thread#run()。
- 当java.lang.Thread#run()执行完成或者中途抛出异常,则终止JavaThread
- 最后释放相关资源(C层面创建的如JavaThread及OSThread相关的内容、锁等)
run()
Thread#run()方法很简单,直接贴代码。
public void run() {
if (target != null) {
target.run();
}
}
在创建线程时我们提到,构造函数在init(...)中会对target属性赋值。
- 当通过继承Thread,调用无参构造方法时,
target == null,因此执行实现类重写的run()。 - 当通过调用有参构造方式时,便会调用Runnable#run()。
同时,我们可以看到,run()其实是一个很简单、很普通的方法,不像start()中对状态的判断,因此我们可以直接多次地调用run()。那直接多次调用run()的后果是什么呢?仅仅是run()中的代码在当前线程中多次被执行(这是一个面试题)。
start()和run()的区别
start() : 它的作用是启动一个新线程,新线程会执行相应的run()方法。start()不能被重复调用。
run() : run()就和普通的成员方法一样,可以被重复调用。单独调用run()的话,会在当前线程中执行run(),而并不会启动新线程!
面试题
以下面试题,请在正文中自行寻找答案
- Java创建线程有几种方式?最好的回答是解释清楚4种使用方式的本质是1种创建方式,一定要理解线程池的前提下,说明白线程池创建的本质,不然会给自己埋雷。
- 实现Runnable接口比继承Thread类的优点?
- 有Runnable接口,为什么还需要Callable?
- new一个线程做了什么?已经创建了OS级别的线程吗?
- start()和run()的实现细节
- 能否多次调用start()和run()?多次调用run()会出现什么情况?