面试官问我:创建线程有几种方式?我笑了
前言
多线程在面试中基本上已经是必问项了,面试官通常会从简单的问题开始发问,然后再一步一步的挖掘你的知识面。
比如,从线程是什么开始,线程和进程的区别,创建线程有几种方式,线程有几种状态,等等。
接下来自然就会引出线程池,Lock,Synchronized,JUC的各种并发包。然后就会引出 AQS、CAS、JMM、JVM等偏底层原理,一环扣一环。
这一节我们不聊其他的,只说创建线程有几种方式。
是不是感觉非常简单,不就是那个啥啥那几种么。
其实不然,只有我们给面试官解释清楚了,并加上我们自己的理解,才能在面试中加分。
正文
一般来说我们比较常用的有以下四种方式,下面先介绍它们的使用方法。然后,再说面试中怎样回答面试官的问题比较合适。
1、继承 Thread 类
通过继承 Thread 类,并重写它的 run 方法,我们就可以创建一个线程。
首先定义一个类来继承 Thread 类,重写 run 方法。 然后创建这个子类对象,并调用 start 方法启动线程。
2、实现 Runnable 接口
通过实现 Runnable ,并实现 run 方法,也可以创建一个线程。
首先定义一个类实现 Runnable 接口,并实现 run 方法。 然后创建 Runnable 实现类对象,并把它作为 target 传入 Thread 的构造函数中 最后调用 start 方法启动线程。
3、实现 Callable 接口,并结合 Future 实现
首先定义一个 Callable 的实现类,并实现 call 方法。call 方法是带返回值的。 然后通过 FutureTask 的构造方法,把这个 Callable 实现类传进去。 把 FutureTask 作为 Thread 类的 target ,创建 Thread 线程对象。 通过 FutureTask 的 get 方法获取线程的执行结果。
4、通过线程池创建线程
此处用 JDK 自带的 Executors 来创建线程池对象。
首先,定一个 Runnable 的实现类,重写 run 方法。 然后创建一个拥有固定线程数的线程池。 最后通过 ExecutorService 对象的 execute 方法传入线程对象。
到底有几种创建线程的方式?
那么问题来了,我这里举例了四种创建线程的方式,是不是说明就是四种呢?
我们先看下 JDK 源码中对 Thread 类的一段解释,如下图。
There are two ways to create a new thread of execution
翻译:有两种方式可以创建一个新的执行线程
这里说的两种方式就对应我们介绍的前两种方式。
但是,我们会发现这两种方式,最终都会调用 Thread.start 方法,而 start 方法最终会调用 run 方法。
不同的是,在实现 Runnable 接口的方式中,调用的是 Thread 本类的 run 方法。我们看下它的源码,
这种方式,会把创建的 Runnable 实现类对象赋值给 target ,并运行 target 的 run 方法。
再看继承 Thread 类的方式,我们同样需要调用 Thread 的 start 方法来启动线程。由于子类重写了 Thread 类的 run 方法,因此最终执行的是这个子类的 run 方法。
所以,我们也可以这样说。在本质上,创建线程只有一种方式,就是构造一个 Thread 类(其子类其实也可以认为是一个 Thread 类)。
而构造 Thread 类又有两种方式,一种是继承 Thread 类,一种是实现 Runnable接口。其最终都会创建 Thread 类(或其子类)的对象。
再来看实现 Callable ,结合 Future 和 FutureTask 的方式。可以发现,其最终也是通过 new Thread(task) 的方式构造 Thread 类。
最后,在线程池中,我们其实是把创建和管理线程的任务都交给了线程池。而创建线程是通过线程工厂类 DefaultThreadFactory 来创建的(也可以自定义工厂类)。我们看下这个工厂类的具体实现。
它会给线程设置一些默认值,如线程名称,线程的优先级,线程组,是否是守护线程等。最后还是通过 new Thread() 的方式来创建线程的。
因此,综上所述。在回答这个问题的时候,我们可以说本质上创建线程就只有一种方式,就是构造一个 Thread 类。(此结论借鉴来源于 Java 并发编程 78 讲 -- 徐隆曦)
个人想法
但是,在这里我想对这个结论稍微提出一些疑问(若有不同见解,文末可留言交流~)。。。
个人认为,如果你要说有 1种、2种、3种、4种 其实也是可以的。重要的是,你要能说出你的依据,讲出它们各自的不同点和共同点。讲得头头是道,让面试官对你频频点头。。
说只有构造 Thread 类这一种创建线程方式,个人认为还是有些牵强。因为,无论你从任何手段出发,想创建一个线程的话,最终肯定都是构造 Thread 类。(包括以上几种方式,甚至通过反射,最终不也是 newInstance 么)。
那么,如果按照这个逻辑的话,我就可以说,不管创建任何的对象(Object),都是只有一种方式,即构造这个对象(Object) 类。这个结论似乎有些太过无聊了,因为这是一句非常正确的废话。
以 ArrayList 为例,我问你创建 ArrayList 有几种方式。你八成会为了炫耀自己知道的多,跟我说,
通过构造方法, List list = new ArrayList();
通过 Arrays.asList("a", "b")
;通过Java8提供的Stream API,如 List list = Stream.of("a", "b").collect(Collectors.toList());
通过guava第三方jar包, List list3 = Lists.newArrayList("a", "b");
等等,仅以上就列举了四种。现在,我告诉你创建 ArrayList 就只有一种方式,即构造一个 ArrayList 类,你抓狂不。
这就如同,我问你从北京出发到上海去有几种方式。
你说可以坐汽车、火车、坐动车、坐高铁,坐飞机。
那不对啊,动车和高铁都属于火车啊,汽车和火车都属于车,车和飞机都属于交通工具。这样就是只有一种方式了,即坐交通工具。
这也不对啊,我不坐交通工具也行啊,我走路过去不行么(我插眼传送也可以啊,就你皮~)。
最后结论就是,只有一种方式,那就是你人到上海即可。这这这,这算什么结论。。。
所以个人认为,说创建线程只有一种方式有些欠妥。
好好的一个技术文,差一点被我写成议论文了。。。
这个仁者见仁智者见智吧。
最后,我们看一下我从网上看到的一个非常有意思的题目。
有趣的题目
问:一个类实现了 Runnable 接口就会执行默认的 run 方法,然后判断 target 不为空,最后执行在 Runnable接口中实现的 run 方法。而继承 Thread 类,就会执行重写后的 run 方法。那么,现在我既继承 Thread 类,又实现 Runnable 接口,如下程序,应该输出什么结果呢?
public class TestThread {
public static void main(String[] args) {
new Thread(()-> System.out.println("runnable")){
@Override
public void run() {
System.out.println("Thread run");
}
}.start();
}
}
可能乍一看很懵逼,这是什么操作。
其实,我们拆解一下以上代码就会知道,这是一个继承了 Thread 父类的子类对象,重写了父类的 run 方法。然后,父对象 Thread 中,在构造方法中传入了一个 Runnable 接口的实现类,实现了 run 方法。
现在执行了 start 方法,必然会先在子类中寻找 run 方法,找到了就会直接执行,不会执行父类的 run 方法了,因此结果为:Thread run 。
若假设子类没有实现 run 方法,那么就会去父类中寻找 run 方法,而父类的 run 方法会判断是否有 Runnable传过来(即判断target是否为空),现在 target 不为空,因此就会执行 target.run 方法,即打印结果:runnable。
所以,上边的代码看起来复杂,实则很简单。透过现象看本质,我们就会发现,它不过就是考察类的父子继承关系,子类重写了父类的方法就会优先执行子类重写的方法。
和线程结合起来,如果对线程运行机制不熟悉的,很可能就会被迷惑。
推荐阅读:
喜欢我可以给我设为星标哦