目录
暴论还是真理?
先给个看起来像暴论的结论:
创建线程有且只有一种方式,那就是
new 一个 Thread 对象,并给它一个 Runnable 类型的对象
Thread 对象负责启动线程,Runnable 对象负责执行线程
What?创建线程不是有四种方式吗?
- 继承
Thread
类 - 实现
Runnable
接口 - 实现
Callable
接口 - 使用线程池
怎么会是只有一种?
本文深入分析 Java 中跟创建对象有关的类的源码,用类比的方式进行讲解。
深入浅出,一步步证实这个“暴论”。
1. Thread与Runnable—老板与员工
Runnable
和 Thread
就像员工和店老板的关系,一个有技术没店面,一个有店面没技术。
技术是 run()
方法,因为线程要执行的代码都写在里面;店面是 start()
方法,因为线程通过它才能运行起来,没有店面有技术也无用武之地。
谁有技术(run()
)?谁有店面(start()
)?
通过分析 Runnable
和 Thread
的源码,可以得到以下两个结论:
- 只有
Runnable
有run()
,它是只有技术的员工。归根结底,永远是Runnable
的实现对象在执行run()
(店里干活的始终是Runnable
),因为Thread
类没有run()
方法(老板没技术,没法干活)。 - 只有
Thread
有start()
,它是只有店面的老板。Runnable
的实现对象无法脱离Thread
类而独立地run()
,因为它自己没有start()
方法(没有店面光有技术,去哪干活呢?)。
有人可能说,当用继承 Thread
类的方式创建线程时,只有 Thread
没有 Runnable
,不也能创建线程吗?
博客:Java中创建线程的四种方式
继承 Thread
类的方式,看起来好像没有用到 Runnable
,其实看 Thread
的源码会发现, Thread
本身就继承了 Runnable
接口!
创建 Thread
的子类对象,并重写其 run()
方法,其实就相当于是让店老板的儿子当员工罢了!
1.1. 为什么Thread是老板、Runnable是员工?
为什么说只有 Runnable
有 run()
、只有 Thread
有 start()
?
首先,看 Runnable
的源码,很显然它没有 start()
方法。事实上,它除了 run()
方法啥也没有。所以它不得不依赖于 Thread
去帮它启动。
而另一边,Thread
看起来好像是有 run()
方法,但是查看源码,会发现原来其中是这样的:
其实没别的,就是直接调用 target
的 run()
方法,而这个 target
又是什么呢?
呵,原来还不就是 Runnable
的类型的对象(也就是店老板 Thread
的员工)。
怎么证明这个 target
就是店老板招的员工呢?
Thread
类在创建对象的时候,需要传入一个 Runnable
类型的参数:
Thread thread = new Thread(myRunnable); // 把一个 Runnable 实现类的对象作为参数放入 Thread 类的构造器,从而创建 Thread 类的对象
看源码可以发现传入的 Runnable
实现类的对象又被直接传给了 init()
方法。
而这个 init()
方法里面有这么一句:
所以,其实就是这里用传入的 Runnable
实现类的对象给 target
赋值的。
也就是说,Thread
的 run()
方法不过是中转一下罢了,其实还是在调用 Runnable
的实现类的 run()
方法!也就是前面说的,归根结底永远是 Runnable
在干活。
店老板看起来在干活,其实他是背地里让员工在干活!
1.2. 老板与员工的关系
综上所述,Runnable
和 Thread
是无法分开的,Thread
自己继承了 Runnable
,Thread
的 run()
方法调用的又是 Runnable
的实现类的 run()
。
一个是提供店面的店老板(Thread
),一个是提供技术的员工(Runnable
),没了谁店(线程)都开不了。
因此,不管用哪种方式创建线程,总是用 Thread.start()
去启动、用 Runnable.run()
去真正执行。
可能又有人会说,实现 Callable
接口创建线程的方式中就不是用 Runnable.run()
去执行的啊,用的是 Callable.call()
啊?
下面就说说为什么,Callable
其实是在“偷偷地”执行它的 call()
方法,明面上还得是执行 Runnable.run()
!
2. 打黑工的Callable是如何“潜入”Thread的
如果说 Runnable
是个打工的,那么 Callable
就是个打黑工的!
比打工的还不如!
Runnable
和 Callable
看起来像是两个没什么关系的接口,其实它们是有着内在的联系的。
首先,Callable
和 Runnable
一样,都是有技术没店面的打工仔。
它们两个接口一个有 call()
方法、一个有 run()
方法,这两个方法都是要被实现并在里面写线程要执行的逻辑的;但是它们两个接口又都没法自己启动(没有 start()
方法),都需要依赖于 Thread
来给自己提供店面(start()
方法)。
然而,Thread
只收 Runnable
类型! Callable
倒是想给 Thread
打工,人家还不要你呢!
查看 Thread
的源码可以发现,Thread
接口有很多构造方法,但是传入参数都要求是 Runnable
类型的,没有一个构造方法可以传入 Callable
类型的。
而且,Thread
把传入的 Runnable
类型的参数作为自己的 target
属性,并且只能调用 target
的 run()
方法,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
间接实现了 RunnableFuture
接口,而 RunnableFuture
接口又继承了 Runnable
接口。
身份证有了,工作证呢?
FutureTask
中也有 Thread
要调用的 run()
方法,这样一来,身份证(Runnable
身份)和工作证(run()
方法)就都有了。
然而,其内部其实还是调用 Callable
的 call()
方法,好一波暗度陈仓!
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
提交产品(获取返回值)的呢?
还是看 FutureTask
的 run()
方法的源码:
原来 Callable
对象的返回值 result
是先交给 FutureTask
,然后 FutureTask
调用自己的 set()
方法来处理 result
。
在 set()
方法中,FutureTask
把这个返回值赋值给了自己的属性 outcome
,从而暂时存储了起来。
那么当程序调用 FutureTask
的 get()
方法时,是怎么获取存储在 outcome
中的返回值的呢?
FutureTask
的 get()
方法的源码是这样的:
可以看到,返回的是 report(s)
的返回值,而 report()
方法又做了什么呢?
哦!返回了存储着线程返回值的 outcome
!
这样一来,FutureTask
就成功地帮 Callable
做到了两件事:
- 帮它把产品(返回值)暂存起来,当
Callable
中的call()
方法运行完成时,帮它暂时存储返回值。 - 帮它转交产品(返回值),当程序要获取
Callable
的返回值时,把之前暂存的返回值返回。
3. 行业管理者–线程池
创建线程还有一种方式,即使用线程池的方式,在这种方式中有没有用到 Thread
和 Runnable
呢?
当然!创建线程本质上只有一种方式! 线程池也不例外!
创建线程池一般使用工具类 Executors
,比如下面的方式:
ThreadPoolExecutor executorService = (ThreadPoolExecutor) Executors.newFixedThreadPool(10); // 创建固定线程数量的线程池
MyRunnable myRunnable = new MyRunnable(); // 创建实现了 Runnable 的类的对象
executorService.execute(myRunnable); // 把实现了 Runnable 的类的对象作为参数放入 execute() 方法
从代码上就可以看到 Runnable
已经出现了(当然也可以用 Callable
创建线程池,但是它与 Runnable
的关系上面已经说的很清楚了)。
但是表面代码上没看到 Thread
的身影,也没看到 Thread
与 Runnable
的关系。
那让我们看看线程池的 .execute()
方法内部的源代码是怎么样的:
传入的 Runnable
对象又被传给了 addWorker()
方法。
在 addWorker()
方法中,这个 Runnable
对象又被传给了一个 Worker
类的构造器,用于创建一个 Worker
对象。
这个 Worker
类的构造器开始创建线程了,怎么创建的呢?调用一个 newThread()
方法。
这个 newThread()
方法可终于到底了。
果然,还是 new
一个线程,并传给它一个 Runnable
对象。
所以,线程池像是一个行业的管理者,管理着很多店铺。
但是每个店铺中发生的事情,还仍然是店老板(Thread
)和员工(Runnable
或 Callable
)之间的故事!
4. 总结
Thread
:店老板,店面在他手里,有技术的人都得靠他提供用武之地。Runnable
:打工仔,有技术,没店面,想施展身手需要依附于店老板(Thread
)。Callable
:打黑工的,有技术,没店面,连身份证(Runnable
身份)和工作证(run()
身份)都没有!想打工还得通过中介包装,混入店老板(Thread
)的店里干活,做好的产品(返回值)也得通过中介提交。好惨的Callable
。FutureTask
:中介,帮打黑工的(Callable
)包装身份,帮他提交产品(获取返回值),有身份证(Runnable
身份),有工作证(run()
身份)。- 线程池:行业管理者,管理着许多店铺,但是不影响这些店铺的组成方式(老板 + 打工仔)。
至此,所有跟创建线程相关的角色都已经出场了,经过挨个分析源码和它们之间的关系,最终印证了我们的终极结论:
创建线程有且只有一种方式,那就是
new 一个 Thread 对象,并给它一个 Runnable 类型的对象
Thread 对象负责启动(start)线程,Runnable 对象负责执行(run)线程