16.2 线程的创建和启动
16.2 线程的创建和启动
线程对象都必须是Thread类的实例
Java
使用Thread
类代表线程,所有的线程对象都必须是Thread
类或其子类的实例。
每个线程的作用是完成一定的任务,实际上就是执行一段程序流(一段顺序执行的代码)。Java
使用线程执行体来代表这段程序流。
16.2.1 继承Thread类创建线程类
继承Thread来创建并启动多线程的步骤
通过继承Thread
类来创建并启动多线程的步骤如下:
- 定义
Thread
类的子类,并重写该类的run()
方法,该run()
方法的方法体就代表了线程需要完成的任务。因此把run()
方法称为线程执行体。 - 创建
Thread
子类的实例,即创建了线程对象 - 调用线程对象的
start()
方法来启动该线程。
程序 继承Thread类实现多线程
下面程序示范了通过继承Thread
类来创建并启动多线程。
1 | // 1.通过继承Thread类来创建线程类 |
部分运行结果
1 | Thread-0 0 |
上面程序中的FirstThread
类继承了Thread
类,并实现了run()
方法,该run()
方法里的代码执行流就是该线程所需要完成的任务。
主线程
当Java
程序开始运行后,程序至少会创建一个主线程,main()
方法的方法体就是主线程的线程执行体
Thread类常用方法
除此之外,上面程序还用到了线程的如下两个方法。
方法 | 描述 |
---|---|
static Thread currentThread() |
返回当前正在执行的线程对象 |
String getName() |
返回调用该方法的线程名字 |
void setName(String name) |
为线程设置名字 |
在默认情况下,主线程的名字为main
,用户启动的多个线程的名字依次为Thread-0
、Thread-1
、Thread-2
、…、Thread-n
等。
继承Thread类创建线程的缺点
使用继承Thread
类的方法来创建线程类时,多个线程之间无法共享线程类的实例变量
.
16.2.2 实现Runnable接口创建线程类
实现Runnable接口来创建并启动多线程的步骤
- 定义
Runnable
接口的实现类,并重写该接口的run()
方法,**该run()
方法的方法体同样是该线程的线程执行体
**。 - 创建
Runnable
实现类的实例,并以此实例作为Thread
的target
来创建Thread
对象- 该
Thread
对象才是真正的线程对象。 - 也可以在创建
Thread
对象时为该Thread
对象指定一个名字
- 该
- 调用线程对象的
start()
方法来启动该线程。
Thread类构造器 | 描述 |
---|---|
Thread(Runnable target) |
Allocates a new Thread object. |
Thread(Runnable target, String name) |
Allocates a new Thread object. |
程序 实现Runable接口来创建并启动多线程
下面程序示范了通过实现Runnable
接口来创建并启动多线程。
1 | // 通过实现Runnable接口来创建线程类 |
上面程序中实现了run()
方法,也就是定义了该线程的线程执行体。
如何获取当前线程对象
- 通过继承
Thread
类来获得当前线程对象
比较简单,直接使用this
关键字就可以了; - 但通过实现
Runnable
接口来获得当前线程对象,则必须使用Thread.currentThread()
方法。
Runnable接口是函数式接口
Runnable
接口中只包含一个抽象方法,从Java 8
开始,Runnable
接口使用了@FunctionalInterface
修饰。也就是说, Runnable
接口是函数式接口,可使用Lambda
表达式创建Runnable
对象。接下来介绍的Callable
接口也是函数式接口。
多个线程共享一个target则可以共享target中的实例变量
采用Runnable
接口的方式创建的多个线程可以共享线程类的实例变量。这是因为只创建了一个target
实例,而多个线程可以共享这个target
实例,因而多个线程中的实例变量也是共享的。
部分运行效果
1 | 新线程2 82 |
从上面的运行结果可以看出:两个子线程的i变量是连续的,也就是采用Runnable
接口的方式创建的多个线程可以共享线程类的实例变量。这是因为在这种方式下,程序所创建的Runnable
对象只是线程的target
,而多个线程可以共享同一个target
,所以多个线程可以共享同一个线程类(实际上应该是线程的target
类)的实例变量。
16.2.3 使用Callable和Future创建线程
通过实现Runnable
接口创建多线程时, Thread
类的作用就是把Runnable
实例提供的run()
方法包装成线程执行体。
那么是否可以直接把任意方法都包装成线程执行体呢?Java
目前不行,不过C#可以把任意方法包装成线程执行体,包括有返回值的方法
Callable接口简介
从Java 5
开始,Java
提供了Callable
接口,可以把该接口看成Runnable
接口的增强版,Callable
接口提供了一个call()
方法可以作为线程执行体,但call()
方法比run()
方法功能更强大,具体表现为:
call()
方法可以有返回值。call()
方法可以声明抛出异常。
Callable 接口的call 方法 |
描述 |
---|---|
V call() |
Computes a result, or throws an exception if unable to do so. |
Callable接口实例不能作为Thread构造方法的参数
Callable
接口是Java 5
新增的接口,但Callable
接口不是Runnable
接口的子接口,所以**Callable
接口实例不能直接作为Thread
的target
**
泛型定义call方法返回值 函数式接口
Callable
接口有泛型限制,Callable
接口里的泛型形参类型与call()
方法返回值类型相同。
并且Callable
接口是函数式接口,因此可使用Lambda
表达式创建Callable
对象。
Future接口
FutureTask实现类
Java 5
提供了Future
接口来代表Callable
接口里call()
方法的返回值,并为Future
接口提供了一个FutureTask
实现类,FutureTask
即实现了Future
接口,又实现了Runnable
接口,所以**FutureTask
可以作为Thread
类的target
**。
方法 | 描述 |
---|---|
FutureTask(Callable<V> callable) |
Creates a FutureTask that will, upon running, execute the given Callable. |
Future接口方法
在Future
接口里定义了如下几个公共方法来控制它关联的Callable
任务。
获取call方法的返回值 get方法
方法 | 描述 |
---|---|
V get() |
返回Callable 任务里call 方法的返回值。调用该方法将导致程序阻塞 ,必须等到子线程结束后才会得到返回值。 |
V get(long timeout, TimeUnit unit) |
返回Callable 任务里call 方法的返回值。该方法让程序最多阻塞timeout 和unit 指定的时间,如果经过指定时间后Callable 任务依然没有返回值,将会抛出TimeoutException 异常。 |
判断方法
方法 | 描述 |
---|---|
boolean cancel(boolean mayInterruptIfRunning) |
试图取消该Future 里关联的Callable 任务。 |
boolean isCancelled() |
如果在Callable 任务正常完成前被取消,则返回true 。 |
boolean isDone() |
如果Callable 任务已完成,则返回true 。 |
创建并启动有返回值的线程的步骤
创建并启动有返回值的线程的步骤如下。
- 创建
Callable
对象- 先创建
Callable
接口的实现类,并实现call()
方法,该call()
方法将作为线程执行体,且该call()
方法有返回值,再创建Callable
实现类的对象。 - 直接使用
Lambda
表达式创建Callable
对象。
- 先创建
- 创建
FutureTask
类对象时传入Callable
对象作为构造器参数,FutureTask
对象封装了该Callable
对象的call()
方法的返回值. - 创建
Thread
类对象时传入FutureTask
对象作为构造器参数,然后启动新线程。 - 调用
FutureTask
对象的get()
方法来获得子线程执行结束后的返回值。
程序 带返回值的线程 Lambda表达式写法
下面程序通过实现Callable
接口来实现线程类,并启动该线程。
1 | import java.util.concurrent.*; |
上面程序中使用Lambda
表达式直接创建了Callable
对象,这样就无须先创建Callable
实现类,再创建Callable
对象了。
Callable和Runnable的区别
实现Callable
接口与实现Runnable
接口并没有太大的差别,只是Callable
的call()
方法允许声明抛出异常,而且允许带返回值。
- 上面程序先使用
Lambda
表达式创建一个Callable
接口实例, - 然后将
Callable
接口实例包装成一个FutureTask
对象。 - 主线程中当循环变量
i
等于20时,程序启动以FutureTask
对象为target
的线程。 - 程序最后调用
FutureTask
对象的get()
方法来返回call()
方法的返回值,get()
方法将导致主线程被阻塞,直到call()
方法结束并返回为止。
运行上面程序,将看到主线程和call()
方法所代表的线程交替执行的情形,程序最后还会输出call()
方法的返回值:
1 | main 的循环变量i的值:0 |
程序 带返回值的线程 经典写法
1 | package demo.thread; |
运行结果:
1 | 启动线程 |
打印“启动线程”线程后,主线程阻塞3秒,
然后输出“线程返回值:HelloWorld”
最后输出“main end”
16.2.4 创建线程的三种方式对比
通过继承Thread
类或实现Runnable
、Callable
接口都可以实现多线程,不过实现Runnable
接口与实现Callable
接口的方式基本相同,只是Callable
接口里定义的方法有返回值,可以声明抛出异常而已。因此可以将实现Runnable
接口和实现Callable
接口归为一种方式。这种方式与继承Thread
方式之间的主要差别如下:
实现Runnable,Callable接口方式创建多线程的优缺点
优点
- 线程类只是实现了
Runnable
接口或Callable
接口,还可以继承其他类 - 在这种方式下,多个线程可以共享同一个
target
对象,所以非常适合多个相同线程来处理同份资源的情况,从而可以将CPU
、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
缺点
- 编程稍稍复杂,如果需要访问当前线程,则必须使用
Thread.currentThread()
方法.
继承Thread类的方式创建多线程的优缺点
优点
- 编写简单,如果需要访问当前线程,则无须使用
Thread.currentThread()
方法,直接使用this
关键字即可获得当前线程。
缺点
- 因为线程类已经继承了
Thread
类,所以不能再继承其他父类。
总结
一般推荐采用实现Runnable
接口、Callable
接口的方式来创建多线程。