彻底理解Java线程的创建 深入源码 生动分析

暴论还是真理?

先给个看起来像暴论的结论:

创建线程有且只有一种方式,那就是

new 一个 Thread 对象,并给它一个 Runnable 类型的对象

Thread 对象负责启动线程,Runnable 对象负责执行线程

What?创建线程不是有四种方式吗?

  1. 继承 Thread
  2. 实现 Runnable 接口
  3. 实现 Callable 接口
  4. 使用线程池

怎么会是只有一种?

本文深入分析 Java 中跟创建对象有关的类的源码,用类比的方式进行讲解。
深入浅出,一步步证实这个“暴论”。

1. Thread与Runnable—老板与员工

RunnableThread 就像员工店老板的关系,一个有技术没店面,一个有店面没技术。
技术是 run() 方法,因为线程要执行的代码都写在里面;店面是 start() 方法,因为线程通过它才能运行起来,没有店面有技术也无用武之地。

谁有技术(run())?谁有店面(start())?

通过分析 RunnableThread 的源码,可以得到以下两个结论:

  • 只有 Runnablerun(),它是只有技术的员工。归根结底,永远是 Runnable 的实现对象在执行 run()(店里干活的始终是 Runnable),因为 Thread 类没有 run() 方法(老板没技术,没法干活)。
  • 只有 Threadstart(),它是只有店面的老板Runnable 的实现对象无法脱离 Thread 类而独立地 run(),因为它自己没有 start() 方法(没有店面光有技术,去哪干活呢?)。

有人可能说,当用继承 Thread 类的方式创建线程时,只有 Thread 没有 Runnable,不也能创建线程吗?
博客:Java中创建线程的四种方式

继承 Thread 类的方式,看起来好像没有用到 Runnable,其实看 Thread 的源码会发现, Thread 本身就继承了 Runnable 接口
Thread继承了Runnable
创建 Thread 的子类对象,并重写其 run() 方法,其实就相当于是让店老板的儿子当员工罢了!

1.1. 为什么Thread是老板、Runnable是员工?

为什么说只有 Runnablerun()只有 Threadstart()

首先,看 Runnable 的源码,很显然它没有 start() 方法。事实上,它除了 run() 方法啥也没有。所以它不得不依赖于 Thread 去帮它启动。
Runnable只有run方法
而另一边,Thread 看起来好像是有 run() 方法,但是查看源码,会发现原来其中是这样的:
在这里插入图片描述
其实没别的,就是直接调用 targetrun() 方法,而这个 target 又是什么呢?
target
呵,原来还不就是 Runnable 的类型的对象(也就是店老板 Thread 的员工)。

怎么证明这个 target 就是店老板招的员工呢?
Thread 类在创建对象的时候,需要传入一个 Runnable 类型的参数:

Thread thread = new Thread(myRunnable);  // 把一个 Runnable 实现类的对象作为参数放入 Thread 类的构造器,从而创建 Thread 类的对象

看源码可以发现传入的 Runnable 实现类的对象又被直接传给了 init() 方法。
target传给init方法
而这个 init() 方法里面有这么一句:
init中的target
所以,其实就是这里用传入的 Runnable 实现类的对象给 target 赋值的。

也就是说,Threadrun() 方法不过是中转一下罢了,其实还是在调用 Runnable 的实现类的 run() 方法!也就是前面说的,归根结底永远是 Runnable 在干活

店老板看起来在干活,其实他是背地里让员工在干活!

1.2. 老板与员工的关系

综上所述,RunnableThread无法分开的,Thread 自己继承了 RunnableThreadrun() 方法调用的又是 Runnable 的实现类的 run()

一个是提供店面的店老板Thread),一个是提供技术的员工Runnable),没了谁店(线程)都开不了。
因此,不管用哪种方式创建线程,总是用 Thread.start() 去启动、用 Runnable.run() 去真正执行。

可能又有人会说,实现 Callable 接口创建线程的方式中就不是用 Runnable.run() 去执行的啊,用的是 Callable.call() 啊?

下面就说说为什么,Callable 其实是在“偷偷地”执行它的 call() 方法,明面上还得是执行 Runnable.run()

2. 打黑工的Callable是如何“潜入”Thread的

如果说 Runnable 是个打工的,那么 Callable 就是个打黑工的
比打工的还不如!

RunnableCallable 看起来像是两个没什么关系的接口,其实它们是有着内在的联系的。

首先,CallableRunnable 一样,都是有技术没店面的打工仔。
它们两个接口一个有 call() 方法、一个有 run() 方法,这两个方法都是要被实现并在里面写线程要执行的逻辑的;但是它们两个接口又都没法自己启动(没有 start() 方法),都需要依赖于 Thread 来给自己提供店面(start() 方法)。

然而,Thread 只收 Runnable 类型! Callable 倒是想给 Thread 打工,人家还不要你呢!

查看 Thread 的源码可以发现,Thread 接口有很多构造方法,但是传入参数都要求是 Runnable 类型的,没有一个构造方法可以传入 Callable 类型的。
而且,Thread 把传入的 Runnable 类型的参数作为自己的 target 属性,并且只能调用 targetrun() 方法Thread 的源码里从来就没出现过 call() 方法

这就像来店里打工需要两个证,一个是身份证(Runnable 类型),一个是工作证(run() 方法),缺一不可。

这让 Callable 怎么办呢?它两个证都没有!

2.1. 黑中介FutureTask

Callable 找了个中介来帮他包装自己,也就是 FutureTask 类,这个中介可是证件齐全

它把自己传入 FutureTask 的构造器,把自己包装到 FutureTask 中,然后把 FutureTask 对象传给 Thread ,从而藏在 FutureTask 里面混进了 Thread

MyCallable myCallable = new MyCallable();  // 实现了 Callable 接口的类的对象,现在还进不去 Thread 的构造器
FutureTask<Integer> futureTask = new FutureTask<>(myCallable);  // 藏到 FutureTask 类型的对象中
Thread thread = new Thread(futureTask);  // 成功混入 Thread 中

2.2. 中介FutureTask的身份证和工作证从哪来的?

为什么 FutureTask 类的对象可以进入 Thread 的构造方法?传入 Thread 的参数不是必须是 Runnable 类型的吗?
FutureTask 的身份证哪来的?

其实这是因为 FutureTask 间接实现了 Runnable 接口
FutureTask的实现
FutureTask 间接实现了 RunnableFuture 接口,而 RunnableFuture 接口又继承了 Runnable 接口。
RunnableFuture的继承

身份证有了,工作证呢?
FutureTask 中也有 Thread 要调用的 run() 方法,这样一来,身份证(Runnable 身份)工作证(run() 方法)就都有了。
然而,其内部其实
还是调用 Callablecall() 方法
,好一波暗度陈仓
暗度陈仓

2.3. 打黑工的Callable如何提交产品?

Callable 区别于 Runnable 的主要特点就是可以有返回值,但是它是个打黑工的,它做出的产品(返回值)不能自己直接去提交,会被发现的!

所以还是得靠中介 FutureTask 来帮他提交产品(获取返回值)。

try {
	Object sumObject = futureTask.get();  // 通过 futureTask 获取线程的返回值
	System.out.println(sumObject);  // 使用返回值
} catch (InterruptedException | ExecutionException e) {
	throw new RuntimeException(e);
}

那么 FutureTask 是怎么帮 Callable 提交产品(获取返回值)的呢?

还是看 FutureTaskrun() 方法的源码:
result的暂存

原来 Callable 对象的返回值 result 是先交给 FutureTask ,然后 FutureTask 调用自己的 set() 方法来处理 result
set方法处理result

set() 方法中,FutureTask 把这个返回值赋值给了自己的属性 outcome,从而暂时存储了起来。

那么当程序调用 FutureTaskget() 方法时,是怎么获取存储在 outcome 中的返回值的呢?
FutureTaskget() 方法的源码是这样的:
get方法的返回值
可以看到,返回的是 report(s) 的返回值,而 report() 方法又做了什么呢?
report方法的返回值
哦!返回了存储着线程返回值的 outcome

这样一来,FutureTask 就成功地帮 Callable 做到了两件事:

  • 帮它把产品(返回值)暂存起来,当 Callable 中的 call() 方法运行完成时,帮它暂时存储返回值。
  • 帮它转交产品(返回值),当程序要获取 Callable 的返回值时,把之前暂存的返回值返回。

3. 行业管理者–线程池

创建线程还有一种方式,即使用线程池的方式,在这种方式中有没有用到 ThreadRunnable 呢?

当然!创建线程本质上只有一种方式! 线程池也不例外!

创建线程池一般使用工具类 Executors,比如下面的方式:

ThreadPoolExecutor executorService = (ThreadPoolExecutor) Executors.newFixedThreadPool(10);  // 创建固定线程数量的线程池
MyRunnable myRunnable = new MyRunnable();  // 创建实现了 Runnable 的类的对象
executorService.execute(myRunnable);  // 把实现了 Runnable 的类的对象作为参数放入 execute() 方法

从代码上就可以看到 Runnable 已经出现了(当然也可以用 Callable 创建线程池,但是它与 Runnable 的关系上面已经说的很清楚了)。

但是表面代码上没看到 Thread 的身影,也没看到 ThreadRunnable 的关系。

那让我们看看线程池的 .execute() 方法内部的源代码是怎么样的:
execute方法
传入的 Runnable 对象又被传给了 addWorker() 方法。
传给addWorker方法
addWorker() 方法中,这个 Runnable 对象又被传给了一个 Worker 类的构造器,用于创建一个 Worker 对象。
addWorker方法
Worker构造器
这个 Worker 类的构造器开始创建线程了,怎么创建的呢?调用一个 newThread() 方法。
调用newThread方法
这个 newThread() 方法可终于到底了。
果然,还是 new 一个线程,并传给它一个 Runnable 对象。
newThread方法
所以,线程池像是一个行业的管理者,管理着很多店铺。
但是每个店铺中发生的事情,还仍然是店老板(Thread)和员工(RunnableCallable)之间的故事!

4. 总结

  • Thread店老板,店面在他手里,有技术的人都得靠他提供用武之地。
  • Runnable打工仔,有技术,没店面,想施展身手需要依附于店老板(Thread)。
  • Callable打黑工的,有技术,没店面,连身份证(Runnable 身份)和工作证(run() 身份)都没有!想打工还得通过中介包装,混入店老板(Thread)的店里干活,做好的产品(返回值)也得通过中介提交。好惨的 Callable
  • FutureTask中介,帮打黑工的(Callable)包装身份,帮他提交产品(获取返回值),有身份证(Runnable 身份),有工作证(run() 身份)。
  • 线程池行业管理者,管理着许多店铺,但是不影响这些店铺的组成方式(老板 + 打工仔)。

至此,所有跟创建线程相关的角色都已经出场了,经过挨个分析源码和它们之间的关系,最终印证了我们的终极结论:

创建线程有且只有一种方式,那就是

new 一个 Thread 对象,并给它一个 Runnable 类型的对象

Thread 对象负责启动(start)线程,Runnable 对象负责执行(run)线程

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ZBH4444

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值