如果想要通过多线程做事,那么如何创建并启动线程就变得非常重要。本文将首先介绍全部3种创建线程的方式及1种启动线程方式(调用线程的.start()方法),之后再对3种方式进行对比。
Java中使用Tread类代表线程,所有线程对象都必须是Thread类或其子类(也就是Java多线程系列文章第1篇中多线程模型中的运转模型)的实例,OOP中真正做事的还是对象。每个线程都有线程任务,称之为线程执行体,其实就是特定代码块。
0.知识体系
1.继承Thread类方式
1)创建步骤
此方式创建线程只需下列3步(1)定义Thread类子类,重写子类run方法,run方法体也就代表线程任务。
(2)创建Thread子类对象,即创建线程对象,此时线程处于其5种状态中的新建状态。
(3)如果启动线程对象,调用其start()方法,此时线程处于其5种状态中的就绪态(此处属于线程启动部分,参见线程启动方式)。
2)步骤演示
//1.1.定义Thread类子类。
class FirstWayThread extends Thread{
// 1.2.重写子类run方法,run方法体也就代表线程任务。
public void run(){
for(int i = 0; i < 23; i++){
//Thread中getName()方法获取当前线程名字,此类为其子类,故也可直接调用getName()方法。
System.out.println(getName() + "--" + i);
}
}
}
public class FirstWayThreadTest{
public static void main(String[] args){
//每个Java程序都至少有一个主线程,主线程执行体由main方法确定而非run方法。
System.out.println(Thread.currentThread().getName() + "开始执行");
//2.创建Thread子类对象,即创建线程对象,此时线程处于其5种状态中的新建状态。
FirstWayThread fwt1 = new FirstWayThread();
//fwt1.setName("Tad");//为线程设置名字
FirstWayThread fwt2 = new FirstWayThread();
//3.如果启动线程对象,调用其start()方法,此时线程处于其5种状态中的就绪态(此处属于线程启动部分,参见线程启动方式)。
fwt1.start();
fwt2.start();
System.out.println(Thread.currentThread().getName() + "结束执行");
}
}
3)补充说明
(1)currentThread()是Thread类静态方法,此方法返回当前正在执行的线程对象。
(2)getName()是Thread类实例方法,此方法返回调用该方法的线程名字。(3)setName(String name)是Thread类实例方法,此方法可以为线程对象设置名字。默认情况下,主线程名字为main,用户创建线程名字依次为Thread-0、Thread-1、...、Thread-n。
2.实现Runnable接口方式
1)创建步骤
(2)创建Runnable实现类对象,并以此对象作为Thread的target创建Thread对象,该对象就是线程对象,此时线程处于其5种状态中的新建状态。
(3)如果启动线程对象,调用其start()方法,此时线程处于其5种状态中的就绪态(此处属于线程启动部分,参见线程启动方式)。
2)步骤演示
//1.1.定义Runnable接口实现类。
class SecondWayThread implements Runnable{
public void run() {
// 1.2.重写run()方法,该方法就是线程任务。
for(int i = 0; i < 23; i++){
//非Thread子类,只能用全名
System.out.println(Thread.currentThread().getName() + "--" + i);
}
}
}
public class SecondWayThreadTest {
public static void main(String[] args) {
//查看主线程情况
System.out.println(Thread.currentThread().getName() + "开始执行");
//2.1.创建Runnable实现类对象;
SecondWayThread swt = new SecondWayThread();
//2.2.并以此对象作为Thread的target创建Thread对象,该对象就是线程对象,此时线程处于其5种状态中的新建状态。
Thread swt1 = new Thread(swt,"New Thread");
Thread swt2 = new Thread(swt);
//3.如果启动线程对象,调用其start()方法,此时线程处于其5种状态中的就绪态(此处属于线程启动部分,参见线程启动方式)。
swt1.start();
swt2.start();
System.out.println(Thread.currentThread().getName() + "结束执行");
}
}
此种方式创建线程最大的好处是多个线程可以共享同一个target。
3.使用Callable和Future接口方式
0)情况说明
实现Runnable接口创建线程,Thread类作用就是把run()方法包装成线程执行体,现在问题是run()方法形式是固定的,没有返回值并且不能抛出异常,如果想把任意方法包装成线程执行体,就需要采用Callable和Future接口。
(1) Callable接口
Callable接口提供了 call()方法作为线程执行体,call()方法两点优势明显胜于run()方法。
a)call()方法可以有返回值。b)call()方法可以抛出异常。
以上可知识完成可以提供Callable对象作为Thread类的Target,线程执行体就是Callable对象的call()方法。现在的问题是Callable接口是Java5新增接口,非Runnable子接口(没有run方法,当然即使提供了run()方法也不行),所以Callable对象不能直接作为Thread的target。而且call()方法有返回值(并且能抛出异常),作为线程执行体被调用,如何获取call()方法的返回值呢?就用到了下面的Future接口。
(2)Future接口
Java 5提供了Future接口代表Callable接口里call()方法的返回值,并为Future接口提供了FutureTask实现类(下面会介绍)。Future定义了如下方法控制其关联的Callable任务。
a) V get():返回Callable任务里call()方法的返回值。调用该方法将导致程序阻塞,必须等到子线程结束后才会得到返回值。
b.) V get(long timeout,TimeUnit unit):返回Callable任务里call()方法的返回值。该方法会让程序最多阻塞timeout和unit指定的时间,如果经过指定时间后Callable任务依然没有返回值,抛出TimeoutException异常。
c.) boolean cancel(boolean mayInterruptIfRunning):试图取消该Future关联的Callable任务。
d.) boolean isCancelled():如果在Callable任务正常完成前被取消,则返回true。
e) boolean isDone():如果Callable任务已完成,则返回true。
注:Callable接口有泛型限制,此接口里的泛型形参类型与call()方法返回值类型相同。
(3)FutureTask实现类
FutureTask实现类实现了Future接口和Runnable两个接口,因此可以作为Thread的target。
2)创建步骤
(1)创建Callable接口实现类,并实现call()方法,该方法将作为线程执行体,且call()方法有返回值。
(2)创建Callable实现类实例,使用FutureTask类包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值。
(3) 创建FutureTask对象作为Thread对象的target,创建线程对象,此时线程处于其5种状态中的新建状态。
(4)如果启动线程对象,调用其start()方法,此时线程处于其5种状态中的就绪态(此处属于线程启动部分,参见线程启动方式)。
(5)调用FutureTask对象的get()方法获得子线程执行结束后的返回值。
3)步骤演示
//1.创建Callable接口实现类,并实现call()方法,该方法将作为线程执行体,且call()方法有返回值。
class ThirdWayThread implements Callable<Integer>{
public Integer call() throws Exception {
int i = 0;
for(; i < 23; i++){
//Thread中getName()方法获取当前线程名字,此类为其子类,故也可直接调用getName()方法。
System.out.println(Thread.currentThread().getName()+" 循环变量i的值 " + i);
}
//执行结束后返回结果
return i;
}
}
public class ThirdWayThreadTest {
public static void main(String[] args) {
//2.创建Callable实现类实例,使用FutureTask类包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值。
ThirdWayThread twt = new ThirdWayThread();
//3.创建FutureTask对象作为Thread对象的target,创建线程对象,此时线程处于其5种状态中的新建状态。
FutureTask<Integer> task = new FutureTask<Integer>(twt);
//4.如果启动线程对象,调用其start()方法,此时线程处于其5种状态中的就绪态(此处属于线程启动部分,参见线程启动方式)。
new Thread(task).start();
try{
//5.调用FutureTask对象的get()方法获得子线程执行结束后的返回值。
System.out.println("子线程返回值:"+task.get());
}catch(Exception e){
e.printStackTrace();
}
}
}
4.线程启动方式
前面3种方法最终都要落实到new Thread对象,因为线程想要执行就必须要有Thread对象,此时的线程状态为新建状态;如果想要线程执行就必须转换其状态为就绪态(等待CPU调度),此时就需要调用其.start()方法。关于此会在下篇文章中有更为显式的表达。
5. 3种线程创建方式对比
1)继承Thread
(1)优点
a.代码编写简单。
b.如果需要访问当前线程,无须使用Thread.currentThread()方法,直接使用this即可获取当前线程。
(2)缺点
a.线程类继承了Thread类,不能再继承其他类,有局限性。
b.把业务模型与具体运转的线程模型混杂在一起,不利于解耦。
2)实现Runnable或Callable
(1)优点
a.完成业务代码与线程类的解耦,线程类都是转换业务类的载体而已。
b.业务类实现Runnable接口或Callable接口,还可继承其它类(这也是接口出现的原因,解决单继承带来弊端)。
c.此方式下,多个线程共享同一target对象,非常适合多个相同线程处理同一资源,形成了运转模型(线程)、业务模型、资源模型清晰的模型,更好体现面向对象。
(2)缺点
a.代码实现复杂。
b.访问当前线程必须使用Thread.currentThread()方法,当然这也是代码实现复杂的原因之一。
综上,一般使用实现Runnable接口或Callable 接口的方式来创建多线程。