本文给出两个简单却很有意思的线程相关的题目
题目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 源码。
学习和工作的时候更多地是学会用,而不是多看源码,了解原理。
通过这个简单的问题,希望大家学习和工作之余可以养成查看源码的习惯,多动手练习,多思考几个为什么。
希望大家读书时,尤其是看博客文章时,不要想当然,多思考下问题的本质。
如果你觉得本文对你有帮助,欢迎点赞评论,你的支持和鼓励是我创作的最大动力。