16.2 线程的创建和启动

16.2 线程的创建和启动

线程对象都必须是Thread类的实例

Java使用Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例
每个线程的作用是完成一定的任务,实际上就是执行一段程序流(一段顺序执行的代码)。Java使用线程执行体来代表这段程序流。

16.2.1 继承Thread类创建线程类

继承Thread来创建并启动多线程的步骤

通过继承Thread类来创建并启动多线程的步骤如下:

  1. 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务。因此run()方法称为线程执行体
  2. 创建Thread子类的实例,即创建了线程对象
  3. 调用线程对象的start()方法来启动该线程。

程序 继承Thread类实现多线程

下面程序示范了通过继承Thread类来创建并启动多线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 1.通过继承Thread类来创建线程类
public class FirstThread extends Thread
{
private int i ;
// 重写run方法,run方法的方法体就是线程执行体
public void run()
{
for ( ; i < 100 ; i++ )
{
// 当线程类继承Thread类时,直接使用this即可获取当前线程
// Thread对象的getName()返回当前该线程的名字
// 因此可以直接调用getName()方法返回当前线程的名
System.out.println(getName() + " " + i);
}
}
public static void main(String[] args)
{
for (int i = 0; i < 100; i++)
{
// 调用Thread的currentThread方法获取当前线程
System.out.println(Thread.currentThread().getName() + " " + i);
if (i == 20)
{
// 2.创建、并启动第一条线程
new FirstThread().start();
// 创建、并启动第二条线程
new FirstThread().start();
}
}
}
}

部分运行结果

1
2
3
4
5
6
7
8
9
10
11
12
Thread-0 0
Thread-1 0
Thread-0 1
main 25
Thread-0 2
Thread-1 1
Thread-0 3
Thread-0 4
main 26
main 27
Thread-0 5
Thread-1 2

上面程序中的FirstThread类继承了Thread类,并实现了run()方法,run()方法里的代码执行流就是该线程所需要完成的任务

主线程

Java程序开始运行后,程序至少会创建一个主线程,main()方法的方法体就是主线程的线程执行体

Thread类常用方法

除此之外,上面程序还用到了线程的如下两个方法。

方法 描述
static Thread currentThread() 返回当前正在执行的线程对象
String getName() 返回调用该方法的线程名字
void setName(String name) 为线程设置名字

在默认情况下,主线程的名字为main,用户启动的多个线程的名字依次为Thread-0Thread-1Thread-2、…、Thread-n等。

继承Thread类创建线程的缺点

使用继承Thread类的方法来创建线程类时,多个线程之间无法共享线程类的实例变量.

16.2.2 实现Runnable接口创建线程类

实现Runnable接口来创建并启动多线程的步骤

  1. 定义Runnable接口的实现类,并重写该接口的run()方法,**该run()方法的方法体同样是该线程的线程执行体**。
  2. 创建Runnable实现类的实例,并以此实例作为Threadtarget来创建Thread对象
    1. Thread对象才是真正的线程对象
    2. 也可以在创建Thread对象时为该Thread对象指定一个名字
  3. 调用线程对象的start()方法来启动该线程。
Thread类构造器 描述
Thread(Runnable target) Allocates a new Thread object.
Thread(Runnable target, String name) Allocates a new Thread object.

程序 实现Runable接口来创建并启动多线程

下面程序示范了通过实现Runnable接口来创建并启动多线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 通过实现Runnable接口来创建线程类
public class SecondThread implements Runnable
{
private int i ;
// run方法同样是线程执行体
public void run()
{
for ( ; i < 100 ; i++ )
{
// 当线程类实现Runnable接口时,
// 如果想获取当前线程,只能用Thread.currentThread()方法。
System.out.println(Thread.currentThread().getName() + " " + i);
}
}

public static void main(String[] args)
{
for (int i = 0; i < 100; i++)
{
System.out.println(Thread.currentThread().getName() + " " + i);
if (i == 20)
{
SecondThread st = new SecondThread(); // ①
// 通过new Thread(target , name)方法创建新线程
new Thread(st , "新线程1").start();
new Thread(st , "新线程2").start();
}
}
}
}

上面程序中实现了run()方法,也就是定义了该线程的线程执行体。

如何获取当前线程对象

  • 通过继承Thread类来获得当前线程对象比较简单,直接使用this关键字就可以了;
  • 但通过实现Runnable接口来获得当前线程对象,则必须使用Thread.currentThread()方法。

Runnable接口是函数式接口

Runnable接口中只包含一个抽象方法,从Java 8开始,Runnable接口使用了@FunctionalInterface修饰。也就是说, Runnable接口是函数式接口,可使用Lambda表达式创建Runnable对象。接下来介绍的Callable接口也是函数式接口。

多个线程共享一个target则可以共享target中的实例变量

采用Runnable接口的方式创建的多个线程可以共享线程类的实例变量。这是因为只创建了一个target实例,而多个线程可以共享这个target实例,因而多个线程中的实例变量也是共享的。

部分运行效果

1
2
3
4
5
6
7
8
9
10
新线程2  82
新线程1 81
新线程2 83
新线程2 85
main 54
main 55
新线程2 86
新线程2 87
新线程1 84
新线程2 88

从上面的运行结果可以看出:两个子线程的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接口实例不能直接作为Threadtarget**

泛型定义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方法的返回值。该方法让程序最多阻塞timeoutunit指定的时间,如果经过指定时间后Callable任务依然没有返回值,将会抛出TimeoutException异常。

判断方法

方法 描述
boolean cancel(boolean mayInterruptIfRunning) 试图取消该Future里关联的Callable任务。
boolean isCancelled() 如果在Callable任务正常完成前被取消,则返回true
boolean isDone() 如果Callable任务已完成,则返回true

创建并启动有返回值的线程的步骤

创建并启动有返回值的线程的步骤如下。

  1. 创建Callable对象
    1. 先创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,且该call()方法有返回值,再创建Callable实现类的对象。
    2. 直接使用Lambda表达式创建Callable对象
  2. 创建FutureTask类对象时传入Callable对象作为构造器参数,FutureTask对象封装了该Callable对象的call()方法的返回值.
  3. 创建Thread类对象时传入FutureTask对象作为构造器参数,然后启动新线程。
  4. 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。

程序 带返回值的线程 Lambda表达式写法

下面程序通过实现Callable接口来实现线程类,并启动该线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import java.util.concurrent.*;

public class ThirdThread
{
public static void main(String[] args)
{
// 1.先使用Lambda表达式创建Callable<Integer>对象
// 2.使用FutureTask来包装Callable对象
FutureTask<Integer> task = new FutureTask<Integer>((Callable<Integer>)() -> {
int i = 0;
for ( ; i < 100 ; i++ )
{
System.out.println(Thread.currentThread().getName()
+ " 的循环变量i的值:" + i);
}
// call()方法可以有返回值
return i;
});

for (int i = 0 ; i < 100 ; i++)
{
System.out.println(Thread.currentThread().getName()
+ " 的循环变量i的值:" + i);
if (i == 20)
{
// 3.实质还是以Callable对象来创建、并启动线程
new Thread(task , "有返回值的线程").start();
}
}
try
{
// 4.获取线程返回值
System.out.println("子线程的返回值:" + task.get());
}
catch (Exception ex)
{
ex.printStackTrace();
}
}
}

上面程序中使用Lambda表达式直接创建了Callable对象,这样就无须先创建Callable实现类,再创建Callable对象了

Callable和Runnable的区别

实现Callable接口与实现Runnable接口并没有太大的差别,只是Callablecall()方法允许声明抛出异常,而且允许带返回值。

  • 上面程序先使用Lambda表达式创建一个Callable接口实例,
  • 然后将Callable接口实例包装成一个FutureTask对象。
  • 主线程中当循环变量i等于20时,程序启动以FutureTask对象为target的线程。
  • 程序最后调用FutureTask对象的get()方法来返回call()方法的返回值,get()方法将导致主线程被阻塞,直到call()方法结束并返回为止

运行上面程序,将看到主线程和call()方法所代表的线程交替执行的情形,程序最后还会输出call()方法的返回值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
main 的循环变量i的值:0
......
main 的循环变量i的值:23
有返回值的线程 的循环变量i的值:0
main 的循环变量i的值:24
有返回值的线程 的循环变量i的值:1
...
main 的循环变量i的值:98
有返回值的线程 的循环变量i的值:72
main 的循环变量i的值:99
有返回值的线程 的循环变量i的值:73
...
有返回值的线程 的循环变量i的值:99
子线程的返回值:100

程序 带返回值的线程 经典写法

G:\dev2\idea_workspace\MyJavaTools\RunableTools\src\demo\thread\CallableThread.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package demo.thread;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class CallableThread implements Callable<String> {
@Override
public String call() throws Exception {
Thread.sleep(3*1000);
return "HelloWorld";
}

public static void main(String[] args) {
CallableThread callable = new CallableThread();
FutureTask<String> futureTask = new FutureTask<>(callable);
Thread thread = new Thread(futureTask);
System.out.println("启动线程");
// 启动线程
thread.start();

// 获取线程的返回值
try {
System.out.println("线程返回值:"+futureTask.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
System.out.println("main end");
}
}

运行结果:

1
2
3
启动线程
线程返回值:HelloWorld
main end

打印“启动线程”线程后,主线程阻塞3秒,
然后输出“线程返回值:HelloWorld”
最后输出“main end”

16.2.4 创建线程的三种方式对比

通过继承Thread类或实现RunnableCallable接口都可以实现多线程,不过实现Runnable接口与实现Callable接口的方式基本相同,只是Callable接口里定义的方法有返回值,可以声明抛出异常而已。因此可以将实现Runnable接口和实现Callable接口归为一种方式。这种方式与继承Thread方式之间的主要差别如下:

实现Runnable,Callable接口方式创建多线程的优缺点

优点

  • 线程类只是实现了Runnable接口或Callable接口,还可以继承其他类
  • 在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。

缺点

  • 编程稍稍复杂,如果需要访问当前线程,则必须使用Thread.currentThread()方法.

继承Thread类的方式创建多线程的优缺点

优点

  • 编写简单,如果需要访问当前线程,则无须使用Thread.currentThread()方法,直接使用this关键字即可获得当前线程。

缺点

  • 因为线程类已经继承了Thread类,所以不能再继承其他父类。

总结

一般推荐采用实现Runnable接口、Callable接口的方式来创建多线程