1.简介
在这阶段的学习过程中我会先抛出一系列:线程如何创建?这是一个很关键的问题,并发的关键在于多线程,那么如何创建线程呢?大概有几种方式呢?这几种方式的区别是什么?什么情况下应该使用这种创建方式?什么时候又不应该呢?那么具体的过程应该是如何呢?是否应该给出一两个例子会更好的说明一下?
问题太多,搞得自己都乱了,最主要的还是要一点点的去了解,最后串成一根线,才能更好对知识的进行掌握。
我想应该将这几种方式联系起来做一个对比,这样才能更好的理解这些创建线程方式的优点与缺点。
按照现有的认识,总的来说有两种实现线程的方式:
- 实现Runnable接口
- 继承Thread类
其实按照我的理解的话,详细分一下的话可以分为三种,就是继承Thread类,实现Runnable接口,实现Callable接口(虽然其内部也是实现Runnable接口),主要就是实现Runnable接口没有返回值,而实现Callable接口可以有返回值,所以也可以按照这三种方式去思考实际开发过程中到底需要哪种创建方式。
2.几种实现方式详解
2.1 继承Thread类
实现:
- 需要实现 run() 方法,因为 Thread 类也实现了
Runable
接口。 - 当调用 start() 方法启动一个线程时,虚拟机会将该线程放入就绪队列中等待被调度,当一个线程被调度时会执行该线程的 run() 方法。
Demo如下:
1 | public class Demo { |
关于start()方法需要注意的有两点:
- 我们在程序里面调用了start()方法后,虚拟机会先为我们创建一个线程,然后等到这个线程第一次得到时间片时再调用run()方法。
- 注意不可多次调用start()方法。在第一次调用start()方法后,再次调用start()方法会抛出异常。
此处我有两个疑惑,根据平时的积累之后给出了答案,问题如下:
start()方法和run()方法的区别?
- 只有调用了start()方法,才会表现出多线程的特性,不同线程的run()方法里面的代码交替执行。如果只是调用run()方法,那么代码还是同步执行的,必须等待一个线程的run()方法里面的代码全部执行完毕之后,另外一个线程才可以执行其run()方法里面的代码。
为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?
- new 一个 Thread,线程进入了新建状态;调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
- 总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。
2.2 实现Runnable接口
实现:
- 实现接口方式
- 需要实现接口中的 run() 方法。
- 使用 Runnable 实例再创建一个 Thread 实例,然后调用 Thread 实例的 run() 方法来启动线程。
- 函数式编程实现方式(
JDK 1.8 +
)- 可以使用函数式编程:
new Thread(() -> { .... }).start();
方便快捷!
- 可以使用函数式编程:
Runnable
是一个函数式接口,这意味着我们可以使用Java 8的函数式编程来简化代码。
首先还是来认识一下函数式编程是个什么东西吧?
函数式编程这是在Java 8 之后才有的,它的声明是通过一个注解来实现的,可以查看Runnable 的接口实现便可知道
1 |
|
Demo如下:
1 | public class Demo { |
输出如下:
1 | MyThread |
需要注意的是实现了Runnable接口(implements Runnable
)要进行使用的时候是使用new MyThread().run();
而不是new Thread().start();
所以实现Runnable接口,我们有两种方式可以去实现创建线程,总的来说,使用匿名内部类的函数式编程会比较方便一点,不用那么多操作,那当然什么方便使用什么了~
上面的实现接口方式还可以这么写,都是一样的,就是知道多个写法
- 需要实现接口中的 run() 方法。
- 使用 Runnable 实例再创建一个 Thread 实例,然后调用 Thread 实例的 start() 方法来启动线程。
Demo如下:
1 | public class MyRunnable implements Runnable{ |
2.3 实现 Callable接口
2.3.1 Callable接口
其实也算是实现Runnable接口!Callable
与Runnable
类似,同样是只有一个抽象方法的函数式接口。不同的是,Callable
提供的方法是有返回值的,而且支持泛型。
1 |
|
那么这个返回值该如何接受呢?这就引出了实现Callable 接口的几种方法了,可以使用下面的Callable+ Future
接口完成,也可以使用再下面的Callable+ FutureTask
类进行实现,一般来说,选择后者的做法居多。
其实这两者的差别就是使用:submit(Runnable task)
和 submit(Callable task)
的差别而已了!
2.3.2 Future接口
Future
接口只有几个比较简单的方法:
1 | public abstract interface Future<V> { |
cancel
方法是试图取消一个线程的执行,但是并不一定能够成功,因为任务可能已完成、已取消、或者一些其它因素不能取消,存在取消失败的可能。boolean
类型的返回值是“是否取消成功”的意思。参数paramBoolean
表示是否采用中断的方式取消线程执行。
所以有时候,为了让任务有能够取消的功能,就使用Callable
来代替Runnable
。如果为了可取消性而使用 Future
但又不提供可用的结果,则可以声明 Future
形式类型、并返回 null
作为底层任务的结果。
那一般是怎么配合使用Callable
的呢?Callable
一般是配合线程池工具ExecutorService
来使用的。
ExecutorService
可以使用submit
方法来让一个Callable
接口执行。它会返回一个Future
,我们后续的程序可以通过这个Future
的get
方法得到结果。
再注意些:下面是通过new Task ();
然后将这个Task使用线程池的submit
进行提交的,submit
是有返回值的,然后使用一个Future<>
进行接收,再在下面我讲到了FutureTask
之后,发现我们使用FutureTask
或者更加方便一些,应该将这两者结合起来,就能更明白FutureTask
的作用了。
简单的使用Future 接口的Demo如下:
1 | // 自定义Callable |
输出结果:
1 | 2 |
在线程池中的使用,可以具体看看我之前写的博客中的一个Demo:Callable+ThreadPoolExecutor,可以申请一个List,用来装返回的数据:List<Future<String>> futureList = new ArrayList<>();
,然后可以对这个futureList
进行遍历输出。
2.3.3 FutureTask类
关于FutureTask的源码分析,我在这篇文章进行了分析:Java 并发 - 多线程:FutureTask源码分析。可以查看一下。
需要注意通过使用Thread 方式配合FutureTask 的方式时,主线程在调用 futureTask.get()
方法时阻塞主线程;然后 Callable 内部开始执行,并返回运算结果;此时 futureTask.get()
得到结果,主线程恢复运行。当然,如果不希望阻塞主线程,可以考虑利用 ExecutorService,把 FutureTask 放到线程池去管理执行。
示例Demo1:
配合Executors 进行使用
1 | // 自定义Callable,与上面一样 |
使用上与第一个Demo有一点小的区别。首先,调用submit
方法是没有返回值的。这里实际上是调用的submit(Runnable task)
方法,而上面的Demo,调用的是submit(Callable task)
方法。
然后,这里是使用FutureTask
直接取get
取值,而上面的Demo是通过submit
方法返回的Future
去取值。
在很多高并发的环境下,有可能Callable 和FutureTask 会创建多次。FutureTask能够在高并发环境下确保任务只执行一次。
示例Demo2:
配合线程Thread 进行使用
1 |
|
输出:
1 | 123 |
还可以参考的Demo3 创建的方式其实一样,就是较为简单易看,这里作为一个参考:
1 | public class FutureTaskExample { |
输出:
1 | other task is running... |
2.4 其他实现线程的方法
Java5 之后的Executors
,Executors
工具类可以用来创建线程池。
Executors
工具类是用来创建线程池的,这个线程池可以指定线程个数,也可以不指定,也可以指定定时器的线程池,它有如下常用的方法:
1 | newFixedThreadPool(int nThreads):创建固定数量的线程池 |
这就涉及线程池的概念了,关于线程池的一些可以查看之前的文章:Java 并发 - 多线程:深入线程池原理
3.深入理解Thread类
3.1 Thread类构造方法
1 | // Thread的构造方法 |
Thread类有很多构造方法,不过都是以上面这个构造方法为基准进行改造的,所以总的来说了解上面这个构造方法就可以了。比如有以下这么几个:
1 | // 无参构造 |
我们大多调用下面两个构造方法:
1 | Thread(Runnable target) |
Thread的构造方法主要有以下的几个参数:
g:线程组,指定这个线程是在哪个线程组下;
target:指定要执行的任务;
name:线程的名字,多个线程的名字是可以重复的。如果不指定名字,见片段2;
acc:用于初始化私有变量
inheritedAccessControlContext
。它是一个私有变量,但是在
Thread
类里只有init
方法对它进行初始化,在exit
方法把它设为null
。其它没有任何地方使用它。一般我们是不会使用它的,那什么时候会使用到这个变量呢?可以参考这个stackoverflow的问题:Restrict permissions to threads which execute third party software;inheritThreadLocals:可继承的
ThreadLocal
,Thread
类里面有两个私有属性来支持`ThreadLocal。
3.2 Thread类的几个常用方法
3.2.1 currentThread
currentThread()
:静态方法,返回对当前正在执行的线程对象的引用;
1 | /** |
像获取当前线程的名字,一般就可以这么使用了:Thread.currentThread().getName();
3.2.2 start
start()
:开始执行线程的方法,java虚拟机会调用线程内的run()方法;
需要注意的是:不可以多次启动线程,而且线程一旦完成执行,就不可以再次启动。意思就是同一个线程多次调用start()
就会出现问题了!
1 | // 线程被执行,JVM调用run方法 |
3.2.3 yield
yield()
:yield()
指的是当前线程愿意让出对当前处理器的占用。这里需要注意的是,就算当前线程调用了yield()
方法,程序在调度的时候,也还有可能继续运行这个线程的;
1 | public static native void yield(); |
3.2.4 sleep
sleep()
:静态方法,使当前线程睡眠一段时间;
1 | // 进行睡眠 线程不会失去任何监视器的所有权。 |
3.2.5 join
join()
:使当前线程等待另一个线程执行完毕之后再继续执行,内部调用的是Object类的wait方法实现的;
1 | // 最多等待millis毫秒,使此线程死亡。如果参数为0则意味着永远等待。 |
4.关于submit与execute
关于execute 的详解请看这里:Java 并发 - 多线程:深入线程池原理,这里列出其源码,方便对比。
1 | public void execute(Runnable command) { |
submit方法则是在ExecutorService接口中定义的。
1 | //ExecutorService |
在其子类AbstractExecutorService实现了submit方法。
submit有Future返回值 :
1 | //AbstractExecutorService |
在AbstractExecutorService实现的submit方法实际上是一个模板方法,定义了submit方法的算法骨架,其execute交给了子类。
尽管submit方法能提供线程执行的返回值,但只有实现了Callable才会有返回值,而实现Runnable的线程则是没有返回值的,也就是说在上面的3个方法中,submit(Callable<T> task)
能获取到它的返回值,submit(Runnable task, T result)
能通过传入的载体result间接获得线程的返回值或者准确来说交给线程处理一下,而最后一个方法submit(Runnable task)
则是没有返回值的,就算获取它的返回值也是null。
从上面的源码可以看到,这三者方法几乎是一样的,关键就在于:
1 | RunnableFuture<T> ftask = newTaskFor(task); |
它是如何将一个任务作为参数传递给了newTaskFor,然后调用execute方法,最后进而返回ftask的呢?
关于newTaskFor(task)
其源码为:
1 | //AbstractExecutorService#newTaskFor |
上面我们已经说了,FutureTask实现了Future和Runnable接口。Future接口是Java线程Future模式的实现,可用用来异步计算,实现Runnable接口表示可以作为一个线程执行。FutureTask实现了这两个接口意味着它代表异步计算的结果,同时可以作为一个线程交给Executor来执行。
两者的主要区别,简略说明有以下几个:
executor 方法,无返回值;submit 方法,可以提供
Future < T >
类型的返回值。excute 方法会抛出异常;sumbit 方法不会抛出异常,除非你调用
Future.get()
。excute 入参Runnable;submit 入参可以为Callable,也可以为Runnable。
注:关于第二种,传入Runnable 和 result 的使用一直有疑惑,这里给出一个Demo
1 | public class Submit2 { |
5.几种比较
5.1 实现接口 VS 继承Thread
这里再稍微总结一下线程创建的两种方式:
- 继承Thread类,并重写run()方法
- 实现Runnable接口,覆盖接口中的run()方法,并把Runnable接口的实现扔给Thread。
1 | public static void main(String[] args) { |
这里抛出一个问题:写的两种创建线程的方式,都涉及到了run()
方法,那么Thread
里的run()
方法具体是怎么实现的吗?
Thread
中的run()
方法里东西很少,就一个 if 判断:
1 |
|
有个target
对象,判断该变量是否为null,非空的时候,去执行target
对象中的run()
方法,否则啥也不干。而这个target
对象,就是我们说的Runnable
:
1 | /* What will be run. */ |
Runnable
类很简单,就一个抽象方法:
1 |
|
这个抽象方法也是run()
!如果我们使用Runnable
,就需要实现这个方法,由于这个Runnable
类上面标了@FunctionalInterface
注解,所以可以使用函数式编程。
这样一来:
- 假如我用第一种方式:继承了
Thread
类,然后重写了run()
方法,那么它就不会去执行上面这个默认的run()
方法了(即不会去判断target
),会执行我重写的run()
方法逻辑。 - 假如我是用的第二种方式:实现
Runnable
接口的方式,那么它会执行默认的run()
方法,然后判断target
不为空,再去执行我在Runnable
接口中实现的run()
方法。
还有个问题:如果我既继承了Thread
类,同时我又实现了Runnable
接口,比如这样,最后会打印什么信息出来呢?
1 | public static void main(String[] args) { |
输出:
1 | Thread run |
其实这个答案很简单,我们来分析一下代码便知:其实是 new 了一个对象(子对象)继承了Thread
对象(父对象),在子对象里重写了父类的run()
方法;然后父对象里面扔了个Runnable
进去,父对象中的run()
方法就是最初那个带有 if 判断的run()
方法。
现在执行start()
后,肯定先在子类中找run()
方法,找到了,父类的run()
方法自然就被干掉了,所以会打印出:Thread run。
如果我们现在假设子类中没有重写run()
方法,那么必然要去父类找run()
方法,父类的run()
方法中就得判断是否有Runnable
传进来,现在有一个,所以执行Runnable
中的run()
方法,那么就会打印:Runnable run 出来。
说白了,就是 Java 语言本身的父子继承关系,会优先执行子类重写的方法而已!
我理解的实现接口会更好一些,因为:
- Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口;
- 类可能只要求可执行就行,继承整个 Thread 类开销过大,如果使用线程时不需要使用Thread类的诸多方法,显然使用
Runnable
接口更为轻量。 Runnable
接口出现更符合面向对象,将线程单独进行对象的封装。Runnable
接口出现,降低了线程对象和线程任务的耦合性。
所以总的来说,还是优先使用实现Runnable
接口方式进行线程的实现较为友好。
5.2 Runnable接口 VS Callable接口
总得来说有两个不一样的地方:
- Runnable 接口不会返回结果或抛出检查异常,但是Callable 接口可以
- Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。
- 如果想任务有能够取消的功能,就使用
Callable
来代替Runnable
Runnable自Java 1.0以来一直存在,但Callable仅在Java 1.5中引入,目的就是为了来处理Runnable不支持的用例。Runnable 接口不会返回结果或抛出检查异常,但是Callable 接口可以。所以,如果任务不需要返回结果或抛出异常推荐使用 Runnable 接口,这样代码看起来会更加简洁。
Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已;
1 |
|
Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。
这其实是很有用的一个特性,因为多线程相比单线程更难、更复杂的一个重要原因就是因为多线程充满着未知性,某条线程是否执行了?某条线程执行了多久?某条线程执行的时候我们期望的数据是否已经赋值完毕?无法得知,我们能做的只是等待这条多线程的任务执行完毕而已。而Callable+Future/FutureTask却可以获取多线程运行的结果,可以在等待时间太长没获取到需要的数据的情况下取消该线程的任务,真的是非常有用。
1 |
|
注意:
- 工具类 Executors 可以实现 Runnable 对象和 Callable 对象之间的相互转换。(
Executors.callable(Runnable task)
或Executors.callable(Runnable task,Object resule)
)。
6.小结
自上面整个流程分析下来,可以很清楚的对几种创建线程方式的理解了,总的来说还是实现Runnable接口比继承Thread更好一些,所以对于我来说比较偏向于使用继承接口,在继承接口有两种可供选择,一种是有返回值的callable,一种是没有返回值的Runnable接口,这就要具体情况具体分析了,看看实际过程中需要使用哪种,对于Thread类也更清晰了,Thread类的几个常用方法也有了一定的理解,总得来说学习过程也更清晰了,还是很开心的。
以上参考
- JAVA中ThreadPoolExecutor线程池的submit方法详解
- 线程池的submit和execute方法区别
- 书籍:深入浅出Java多线程
- 书籍:Java并发编程的艺术