面试中最常被虐的地方一定有并发编程这块知识点,无论你是刚刚入门的大四萌新还是2-3年经验的CRUD怪,也就是说这类问题你最起码会被问3年,何不花时间死磕到底。消除恐惧最好的办法就是面对他,奥利给!(这一系列是本人学习过程中的笔记和总结,并提供调试代码供大家玩耍)
本节提要
本节学习完成,你将会清楚地知道什么是并发编程,并发编程主要解决的问题是什么。并且能够自己完成三种实现线程的方法。(熟悉这块的同学可以选择直接点赞👍完成本章学习哦)
一、 并发编程主要解决问题是什么
不知道大家有没有遇到和我一样的问题,在还没有真正使用这块知识来开发之前,会把前两年常问的双十一案例两者结合到一起,觉得双十一的高并发就是并发编程的产物。这里我想说的是,双十一案例的高并发必然包含有并发编程的内容,但是并发编程不足以解决双十一这种场景,如要学习双十一解决方案的,可以学习下异步化-队列和缓存等知识点。
由于计算机CUP-内存—磁盘三者之间的速度差异较大,导致硬件资源得不到充分的利用,所以高并发编程要解决的问题是最大效率的利用硬件资源,从而达到程序效率提升的效果
。
二、“创建”线程的三种方式
这里为什么要给“创建”打上引号呢?
划重点了
一般情况下我们会说创建线程的方式有三种,一种是创建Thread,一种是实现Runnable接口,一种是使用Futuretask。但是这种说法是不严谨的,经过本案例的学习相信大家自然而然就能得出创建线程只有一种方式那就是构造Thread类
。
(1) Thread
通过继承Thread类,并重写run()方法。话不多说直接上代码。
1 | 复制代码public static class ThreadFirstTime extends Thread { |
可以看到通过new一个Thread对象调用其自带的start
方法,我们的线程就跑起来了
输出:
1 | 复制代码Thread-0create Thread success! |
注意这里的start
方法,后面我们会就这一方法来做进一步解析和探讨
(2)Runnable
通过实现Runnable接口并实现run()
方法来实现线程。代码很简单,直接上
1 | 复制代码 public static class RunnableFirstTime implements Runnable { |
这里通过对比我们发现run()
方法中由于实现方式的不同会有些许差异:
ThreadFirstTime是通过继承Thread
来实现的所以可以直接使用this
关键字,可以看到我们使用this.getName()
来获取当前线程的名称。
而我们这边的RunnableFirstTime是通过实现Runnable
接口来实现run()
方法的所以这边使用Thread.currentThread()
来获取当前的线程对象。
这里要提一句Thread.currentThread()
很常用,也很好用,大家要多多使用哦!
相同的地方也显而易见我们可以看到main方法中我们都是通过创建Thread对象来创建线程,同时使用Thread.start()方法来启动线程
,那是不是印证来我们开头所说的那句断言创建线程只有一种方式那就是构造Thread类
,别急我们再来看看Futuretask
是不是也是这样的。
(3)FutureTask
FutureTask
通过实现Callable
接口,并重写call()
方法,来实现有返回值的一种实现。直接上代码,然后分三步来讲解FutureTask
是如何实现线程的执行单元的。
1 | 复制代码public static class CallerTask implements Callable<String> { |
第一步:
1 | 复制代码public static class CallerTask implements Callable<String> { |
创建任务类,通过实现Callable<String>
接口,重写call()
方法来实现一个任务类,此处可以类比Runnable
接口。但是两者不同的是Runnable
接口实现的是run()
方法是一个void的方法,但是Callable<String>
实现的是call()
可以看到给的例子里面返回参数是String,是一个有返回值的方法。
第二步:
1 | 复制代码 //创建异步任务 |
使用FutureTask
的有参构造方法来创建可供Thread管理的异步任务。
第三步:
和前面两种方式一样,通过new Thread()
来手动创建一个线程,调用Thread.start()来启动线程。
1 | 复制代码 //创建线程,调用Thread.start()启动线程 |
到这一步我们可以清楚地看到通过FutureTask
实现线程,其线程的创建方式也是通过new Thread()
来实现的。所以到这里相信大家应该都理解来开篇说的那个结论了,就是创建线程只有一种方式那就是构造Thread类,其实现方式有三种
。
各位面试官大大也注意啦,千万别再问线程创建的方式有哪几种这样的问题啦,小心被人当场手撕哦~
三、new Thread()是如何一穿三的呢?
我们看一下这个方法到底是何方神圣
1 | 复制代码/** |
首先点进去看下这个方法,相信大家都不敢相信,因为入参是Runnable target
???
此时不妨把光标定位到当前类的声明处,也就是140行
1 | 复制代码public class Thread implements Runnable |
恍然大悟了吧,原来我们的Thread类也是实现类Runanble接口的。
Runnable自然而然是没问题的,毕竟人家才是正主。
同理我们也能够猜到FutureTask
他必然也是实现了Runnable
接口了
1 | 复制代码public class FutureTask<V> implements RunnableFuture<V> |
追踪到类的底层关系,我们可以看到FutureTask
实现的是RunnableFuture
,RunnableFuture
继承的是Runnable
和Future
。
可能有的小伙伴要问了,java不是单继承,多实现吗?这里为什么是多继承呢?
接口是可以多继承的,这点需要注意。
在这里又一个巧妙的地方是选择继承而不是实现Runnable
,其目的是因为
1 | 复制代码@FunctionalInterface |
public abstract void run();
这个抽象方法如果RunnableFuture
使用implements来实现的话,后续我们的FutureTask
也必须实现run()
方法,这与我们的call()
就会有冲突,所以在RunnableFuture
中选择重写run()
在后续的实现类中就不需要来实现run()
方法了。
这里说的比较绕,但是希望大家对着代码多读两遍,这是一个细节的点。
读懂这层关系之后我们就会发现FutureTask
这个对象即可以通过Callable
来实现创建,也可以通过Runnable
来实现创建,说通俗一点就是FutureTask
即支持run()
也支持call()
。
1 | 复制代码 FutureTask<String> futureTask = new FutureTask<>(new CallerTask()); |
对这两者关系感兴趣的同学可以继续追踪看一下这段代码和对应的说明:
1 | 复制代码 /** |
可以发现FutureTask
完全可以实现和Runnable一样的效果,但是我们一般不这么玩,一般来说FutureTask
的cal()
方法才是这个类的作用所在,无返回参数的我们建议直接调用Runnable
接口来实现。
记得点赞👍点关注继续和我一起学习后续的内容哦~
本文转载自: 掘金