Java并发学习之四种线程创建方式的实现与对比

线程创建的几种方式

在并发编程中,最基本的就是创建线程了,那么一般的创建姿势是怎样的,又都有些什么区别

Java并发学习之四种线程创建方式的实现与对比

一般来讲线程创建有四种方式:

  1. 继承Thread

  2. 实现Runnable接口

  3. 实现Callable接口,结合 FutureTask使用

  4. 利用该线程池ExecutorService、Callable、Future来实现

所以本篇博文从布局来讲,分为两部分

  1. 实例演示四种使用方式

  2. 对比分析四种使用方式的异同,以及适合的应用场景

I. 实例演示

目标: 创建两个线程并发实现从1-1000的累加

1. 继承Thread实现线程创建

实现逻辑如下

Java并发学习之四种线程创建方式的实现与对比

输出结果

Java并发学习之四种线程创建方式的实现与对比

一般实现步骤:

  • 继承Thread类

  • 覆盖run()方法

  • 直接调用Thread#start()执行

逻辑比较清晰,只需要注意覆盖的是run方法,而不是start方法

2. 实现Runnable接口方式创建线程

Java并发学习之四种线程创建方式的实现与对比

输出结果

Java并发学习之四种线程创建方式的实现与对比

一般实现步骤:

  • 实现Runnable接口

  • 获取实现Runnable接口的实例,作为参数,创建Thread实例

  • 执行Thread#start()启动线程

说明

相比于继承Thread,这里是实现一个接口,最终依然是借助Thread#start()来启动线程

然后就有个疑问:

两者是否有本质上的区别,在实际项目中如何抉择?

3. 实现Callable接口,结合FutureTask创建线程

Callable接口相比于Runnable接口而言,会有个返回值,那么如何利用这个返回值呢?

demo如下

Java并发学习之四种线程创建方式的实现与对比

输出结果

Java并发学习之四种线程创建方式的实现与对比

一般实现步骤:

  • 实现Callable接口

  • 以Callable的实现类为参数,创建FutureTask实例

  • 将FutureTask作为Thread的参数,创建Thread实例

  • 通过Thread#start启动线程

  • 通过FutreTask#get()阻塞获取线程的返回值

说明

Callable接口相比Runnable而言,会有结果返回,因此会由FutrueTask进行封装,以期待获取线程执行后的结果;

最终线程的启动都是依赖Thread#start

4. 线程池方式创建

demo如下,创建固定大小的线程池,提交Callable任务,利用Future获取返回的值

Java并发学习之四种线程创建方式的实现与对比

输出

Java并发学习之四种线程创建方式的实现与对比

一般实现逻辑:

  • 创建线程池(可以利用JDK的Executors,也可自己实现)

  • 创建Callable 或 Runnable任务,提交到线程池

  • 通过返回的Future#get获取返回的结果

II. 对比分析

1. 分类

上面虽然说是有四种方式,但实际而言,主要划分为两类

  • 继承Thread类,覆盖run方法填写业务逻辑

  • 实现Callable或Runnable接口,然后通过Thread或线程池来启动线程

此外,还有一种利用Fork/Join框架来实现并发的方式,后续专门说明,此处先略过

2. 区分说明

继承和实现接口的区别

先把线程池的方式拎出来单独说,这里主要对比Thread, Callable, Runnable三中方式的区别

个人理解,线程的这两种方式的区别也就只有继承和实现接口的本质区别:

一个是继承Thread类,可以直接调用实例的start()方法来启动线程;另一个是实现接口,需要借助Thread#start()来启动线程

继承因为java语言的限制,当你的任务需要继承一个自定义的类时,会有缺陷;而实现接口却没有这个限制


至于网上很多地方说的实现Runnable接口更利于资源共享什么的,比如下面这种作为对比的

Java并发学习之四种线程创建方式的实现与对比

输出:

Java并发学习之四种线程创建方式的实现与对比

MyRun实现Runnable接口,然后创建一个实例,将这个实例作为多个Thread的参数构造Thread类,然后启动线程,发现这几个线程共享了MyRun#ato变量

然而上面这个实现接口改成继承Thread,其他都不变,也没啥两样

Java并发学习之四种线程创建方式的实现与对比

输出如下

Java并发学习之四种线程创建方式的实现与对比

上面除了说明使用Runnable更利于资源共享神马的,其实并没有之外,还有一个比较有意思的,为什么会输出-1?

如果我这个任务是售票的话,妥妥的就超卖了,这个问题留待后续详解


如果你也是在学习java,给你们推荐一个java学习qun:881-982-957,进群找管理就可以免费领取java最新学习资料以及免费公开课门票,群里都是java党,欢迎初学和进阶中的小伙伴,大家一起学习共同进步!

Runnable, Callable两种区别

这两个就比较明显了,最根本的就是

  • Runnable 无返回结果

  • Callable 有返回结果

从根源出发,就直接导致使用姿势上的区别

举个形象的例子说明两种方式的区别:

小明家今儿没米了,小明要吃饭怎么办?

小明他妈对小明说,去你大爷家吃饭吧,至于小明到底吃没吃着,小妈他妈就不管了,这就是Runnable方式;

小明他妈一想,这一家子都要吃饭,我先炒个菜,让小明去大爷家借点米来,所以就等着小明拿米回来开锅,这就是Callable方式

1.Runnable

Runnable不关心返回,所以任务自己默默的执行就可以了,也不用告诉我完成没有,我不care,您自己随便玩,所以一般使用就是

Java并发学习之四种线程创建方式的实现与对比

换成JDK8的 lambda表达式就更简单了

Java并发学习之四种线程创建方式的实现与对比

2.Callable

相比而言,callbale就悲催一点,没法这么随意了,因为要等待返回的结果,但是这个线程的状态我又控制不了,怎么办?借助FutrueTask来玩,所以一般可以看到使用方式如下:

Java并发学习之四种线程创建方式的实现与对比


Thread启动和线程池启动方式

这个就高端了,线程池一听就感觉厉害了,前面的四中方式有三种都是Thread#start()来启动线程,这也是我们最常用的方式,这里单独说一下线程池的使用姿势

  • 首先是创建一个线程池

  • 利用ExecutorService#submit()提交线程

  • Future<Object>接收返回

Java并发学习之四种线程创建方式的实现与对比

说明,这里提交线程之后,并不表示线程立马就要执行,也不表示一定可以执行(这个留待后续线程池的学习中探讨)


III. 小结

四种创建方式

1. 继承Thread类,覆盖run方法,调用Thread#start启动

2. 实现Runnable接口,创建实例,作为Thread构造参数传入,调用Thread#start启动

Java并发学习之四种线程创建方式的实现与对比

3. 实现Callable接口,创建实例,作为FutureTask<>构造参数创建FutureTask对象,将FutureTask对象作为Thread构造参数传入,调用Thread#start启动

Java并发学习之四种线程创建方式的实现与对比

4. 创建一个线程池,利用ExecutorService#submit()提交线程,Future<Object>接收返回

Java并发学习之四种线程创建方式的实现与对比


区别与应用场景

  • 继承和实现接口的方式唯一区别就是继承和实现的区别,不存在共享变量的问题

  • 需要获取返回结果时,结合 FutureTask和Callable来实现

  • Thread和Runnable的两种方式,原则上想怎么用都可以,个人也没啥好推荐的,随意使用

  • 线程池需要注意线程复用时,对ThreadLocal中变量的串用问题(本篇没有涉及,等待后续补上)

       

猜你喜欢

转载自blog.csdn.net/ajian6/article/details/89856891