一、背景
本文给出两个简单却很有意思的线程相关的题目
题目1:
Java 中有几种创建线程的方式?
如果面试中遇到这个问题,估计很多人会非常开心,然而网上的诸多答案真的对吗?
题目2:
public static void main(String[] args) {
Thread thread = new Thread(() -> System.out.println("Runnable run")) {
@Override
public void run() {
System.out.println("Thread run");
}
};
thread.start();
}
请问运行后输出的结果是啥?
拿到这个问题有些同学可能会懵掉几秒钟,什么鬼…
二、分析
2.1 有几种创建形成的方式
不知道大家想过没有,本质上 JDK 8 中提供了几种创建线程的方式?
可能很多人会讲可以先创建 Runnable 当做参数传给 Thread ,可以写匿名内部类,可以编写 Thread 的子类,可以通过线程池等等。
其实线程池的 Worker 内部还是通过 Thread 执行的,而Worker 中的线程是通过 ThreadFactory 创建,ThreadFactory 最终还是通过构造 Thread 或者 Thread 子类的方式创建线程的。
以 org.apache.tomcat.util.threads.TaskThreadFactory
为例:
/**
* Simple task thread factory to use to create threads for an executor
* implementation.
*/
public class TaskThreadFactory implements ThreadFactory {
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
private final boolean daemon;
private final int threadPriority;
public TaskThreadFactory(String namePrefix, boolean daemon, int priority) {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup();
this.namePrefix = namePrefix;
this.daemon = daemon;
this.threadPriority = priority;
}
@Override
public Thread newThread(Runnable r) {
TaskThread t = new TaskThread(group, r, namePrefix + threadNumber.getAndIncrement());
t.setDaemon(daemon);
t.setPriority(threadPriority);
// Set the context class loader of newly created threads to be the class
// loader that loaded this factory. This avoids retaining references to
// web application class loaders and similar.
if (Constants.IS_SECURITY_ENABLED) {
PrivilegedAction<Void> pa = new PrivilegedSetTccl(
t, getClass().getClassLoader());
AccessController.doPrivileged(pa);
} else {
t.setContextClassLoader(getClass().getClassLoader());
}
return t;
}
}
TaskThread
源码:
public class TaskThread extends Thread {
//...
}
其实从本质上讲只有一种创建对象的方式,就是通过创建 Thread 或者子类的方式。
接下来让我们看下 Thread 类的注释:
/**
* 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. For example, a thread that computes primes
* larger than a stated value could be written as follows:
* <hr><blockquote><pre>
* class PrimeThread extends Thread {
* long minPrime;
* PrimeThread(long minPrime) {
* this.minPrime = minPrime;
* }
*
* public void run() {
* // compute primes larger than minPrime
* . . .
* }
* }
* </pre></blockquote><hr>
* <p>
* The following code would then create a thread and start it running:
* <blockquote><pre>
* PrimeThread p = new PrimeThread(143);
* p.start();
* </pre></blockquote>
* <p>
* 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. The same example in this other
* style looks like the following:
* <hr><blockquote><pre>
* class PrimeRun implements Runnable {
* long minPrime;
* PrimeRun(long minPrime) {
* this.minPrime = minPrime;
* }
*
* public void run() {
* // compute primes larger than minPrime
* . . .
* }
* }
* </pre></blockquote><hr>
* <p>
* The following code would then create a thread and start it running:
* <blockquote><pre>
* PrimeRun p = new PrimeRun(143);
* new Thread(p).start();
* </pre></blockquote>
* <p>
*/
public class Thread implements Runnable {
// 省略其他
}
通过注释我们可以看出有两种创建执行线程的方式:
- 继承 Thread 并且重写 run 方法。
- 实现 Runnable 接口实现 run 方法,并作为参数来创建 Thread。
如果是从这个层面上讲,有两种创建 Thread 的方式,其他方式都是这两种方式的变种。
2.2 运行结果是啥?
2.2.1 回顾
可能很多同学看到这里会有些懵。
如果下面这种写法(写法1):
public static void main(String[] args) {
Thread thread = new Thread(() -> System.out.println("Runnable run"));
thread.start();
}
答案是打印: “Runnable run”
如果这么写(写法2):
public static void main(String[] args) {
Thread thread = new Thread() {
@Override
public void run() {
System.out.println("Thread run");
}
};
thread.start();
}
答案是打印“Thread run”
一起写,这个操作有点风骚…
2.2.2 莫慌
我们可以确定的是 thread.start 调用的是 run 方法,既然这里重写了 run 方法,肯定调用的是咱们重写的 run 方法。
因此答案是 "Thread run“。
为了更好地搞清楚这个问题,咱么看下源码:
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
target 部分:
/* What will be run. */
private Runnable target;
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
// ....
this.target = target;
}
我们再看下默认的 run 方法:
/**
* If this thread was constructed using a separate
* <code>Runnable</code> run object, then that
* <code>Runnable</code> object's <code>run</code> method is called;
* otherwise, this method does nothing and returns.
* <p>
* Subclasses of <code>Thread</code> should override this method.
*
* @see #start()
* @see #stop()
* @see #Thread(ThreadGroup, Runnable, String)
*/
@Override
public void run() {
if (target != null) {
target.run();
}
}
注释说的很清楚,通过构造方法传入 Runnable ,则调用 Runnable的 run 方法,否则啥都不干。
因此这就是为什么写法1 的结果是:“Runnable run”。
如果咱们重写了 run 方法,默认 target 的就失效了,因此结果就是"Thread run“。
如果我想先执行 Runnbale 的 run 方法再执行咱们的打印"Thread run“咋办?
既然上面了解到父类的默认行为是执行构造函数中 Runnbale 对象的 run 方法,咱么先调用 super.run 不就可以了吗:
public static void main(String[] args) {
Thread thread = new Thread(() -> System.out.println("Runnable run")) {
@Override
public void run() {
super.run();
System.out.println("Thread run");
}
};
thread.start();
}
输出结果:
Runnable run
Thread run
其实这个题目重点考察大家有没有认真看过源码,有没有真正理解多态的含义,是否都线程基础有一定的了解。
三、总结
这个问题本质上很简单,实际上很多人第一反应会懵掉,是因为很少主动去看 Thread 源码。
学习和工作的时候更多地是学会用,而不是多看源码,了解原理。
通过这个简单的问题,希望大家学习和工作之余可以养成查看源码的习惯,多动手练习,多思考几个为什么。
希望大家读书时,尤其是看博客文章时,不要想当然,多思考下问题的本质。
如果你觉得本文对你有帮助,欢迎点赞评论,你的支持和鼓励是我创作的最大动力。
本人慕课专栏: 解锁大厂思维:剖析《阿里巴巴 Java 开发手册》,感兴趣的同学可以了解下。