校招面试回忆录:请说出在Java中如何开启一个线程?


上述两张图来自jdk1.8Thread类的注释,真有点八股文答案的意思了。。。。。 总结下来就是:
- 要么继承Thread类并实现它的run方法
- 要么实现Runnable接口,在构造Thread的时候当作参数传进去。
但是仔细思考发现Thread类本身是实现Runnable接口的,那么其实方法一本质也是方法二的变体;这时候有人就会说还有一种创建线程方式------Callable接口;确实是的,在提交线程池任务的时候,为了同时满足异步和获取结果,我们通常将任务包装成Callable丢给线程池,线程池底层使用的Thread。但是当我们仔细寻找Thread类中有没有Callable接口的蛛丝马迹,结果也是找不到的,原因也很简单,如下图所示:
实际上,我们给线程池提交的Callable接口被newTaskFor方法转换成了Ruunbale接口的子类RunnableFuture对象,根据做里氏替换原则,在构造Thread的时候是可以当作参数传进去的。 好像分析到这最终的答案也比较明确了,真相只有一个,实现Runnable接口。 但是,根据我自己的思考,我认为实现Runnable接口,好像并不是创建或者说开启一个线程的关键。因为实现runnable,继承Thread并创建对象也罢,这些行为都并不是发生在要创建的线程中,那么真正发生在创建的线程中的行为应该是执行了Runnable接口run方法的线程;那么触发运行执行了Runnable接口run方法的操作就是Thread的start方法,对该方法追根溯源会到一个jni方法:

也就是说,调用Thread的start方法会调用start0这个jni方法,从而与jvm层面实现的方法进行交互,jvm作为一款虚拟机提供了独立且完整的虚拟计算机环境,那么在Java语言层面的Thread也会映射到jvm的线程管理概念,然后jvm再去将jvm线程概念映射到操作系统内核线程上,这样才能实现真正的被操作系统调度从而获取cpu执行的条件。总的来讲,Java语言层面的Thread被称作用户态线程,jvm负责对该用户态线程与内核态线程进行映射,从而获取被调度的能力。所以我给出的答案是Java中如何开启一个线程是调用Thread对象的start方法,与jvm虚拟机进行交互,实现"注册"和"委托"映射到内核态线程,这里利用jstack来看下:

线程ID:#11=====>线程ID(JVM):tid=0x000000015d066000=====>本地线程ID:nid=0x5803,在这里java的Thread其实到内核态线程映射转换了两次。
也就是说,Runnable的run方法实现逻辑以及Callable的call方法实现逻辑(被封装为FutureTask类后,其实也是在run方法内调用call方法)其实更多的是定义了要执行的任务是什么,而不是创建一个新线程;
换个角度思考,线程池通过对线程的池化,不断的复用Thread,干完一个任务(Runnable)还是会干下一个任务(Runnable)的,不会说是来一个任务,就要创建一个新的线程去做;以下是线程池中线程的run方法调用的runWorker方法,不断的getTask并运行:

那为什么Thread不能直接接受Callable而是要包装成Runnable呢?从源码的注释可以得到一些信息,Thread和Runnable是since jdk1.0的,而Callable是由大神Doug Lea在jdk1.5提交的,在这个jdk版本,Doug Lea还提交了线程池系列的源码,也就是说Callable是线程池生态圈时期诞生的,领先于Thread和Runnable的发行时期,这里笔者大胆推断是更好管理,让Runnable成为定义任务的顶级接口,也有助于逻辑的实现。
面试官:你这回答的和我一分钟前搜的答案不太一样呀,还是回去等通知吧!