2.3 Thread和Runnable区别
现在Thread类和Runnable接口都可以以同一功能的方式来实现多线程,那么Java的实际开发来讲,肯定使用Runnable接口,因为采用这种方式可以有效地避免单继承的局限,但是从结构上也需要来观察Thread与Runnable的联系。首先来观察Thread类的定义:
public class Thread extends Object implements Runnable {}
可以发现Thread类也是Runnable接口的子类,那么在之前继承Thread类的时候实际上覆写的还是Runnable接口的run()方法,于是对于Runnable接口实现的多线程操作的类结构组成如图。
由图所示的类结构图可以发现,作为Runnable的两个实现子类:Thread负责资源调度,而MyThread负责处理真实业务,这样的设计结构类似于代理设计模式。
提示:关于Thread类中对run()方法的覆写。
范例:Thread类的部分源码
public class Thread implements Runnable{
private Runnable target; //真实主题
public Thread(Runnable target){} //构造方法保存target
@Override
public void run(){ //覆写run()
if (target!=null){ //target不为空
target.run(); //调用真实主题对象
}
}
}
在Thread类中会保存有target属性,该属性保存的是Runnable的核心业务主题对象,该对象将通过Thread类的构造方法进行传递。当调用Thread.start()方法启动多线程时也会调用Thread.run()方法,而在Thread.run()方法会判断是否提供有target实例,如果有提供,则调用真实主题的方法。
在实际项目中,多线程开发的本质上是在多个线程可以进行同一资源的抢占与处理。而在1此种结构中Thread描述的是线程对象,而并发资源的描述可以通过Runnable定义,操作如图。
package cn.kuiba.util;
class MyThread implements Runnable{ //线程的主体类
private int ticket = 5; //定义总票数
@Override
public void run(){ //线程的主体方法
for (int x=0;x<100;x++){ //进行100次的卖票处理
if (this.ticket>0){ //有剩余票
System.out.println("卖票,ticket="+this.ticket--);
}
}
}
}
public class Main {
public static void main(String args[]){
MyThread mt=new MyThread(); //定义资源对象
new Thread(mt).start(); //第1个线程启动
new Thread(mt).start(); //第2个线程启动
new Thread(mt).start(); //第3个线程启动
}
}
程序执行结果:
卖票,ticket=5
卖票,ticket=3
卖票,ticket=4
卖票,ticket=1
卖票,ticket=2
本程序利用多线程的并发资源访问实现了一个卖票程序,在程序中准备了5张票,同时设置有3个卖票的线程,当票数有剩余时(this.ticket>0),则进行售票处理,本程序的内存关系如图。
2.4Callable接口实现多线程
使用Runnable接口实现的多线程可以避免单继承局限,但是Runnable接口实现的多线程会存在一个问题:Runnable接口里面提供的run()方法不能返回操作结果。所以为了解决这个问题,从JDK1.5开始对于多线程的实现提供了一个新的接口:java.util.concurrent.Callable,此接口定义如下。
@FunctionalInterface
public interface Callable<v>{
public V call() throws Exception;
}
Callable接口定义的时候可以设置一个泛型,此泛型的类型就是call()方法的返回数据类型,这样的好处是可以避免向下转型所带来的安全隐患。
范例:定义线程主体类
class MyThread implements Callable<String> { //定义线程主体类
@Override
public String call() throws Exception { //线程执行方法
for (int x = 0; x < 10; x++) {
System.out.println("********线程执行、x="+x);
}
return "www.kuiba.cn"; //返回结果
}
}
本程序利用Callable接口实现了一个多线程的主体类,并且在call()方法中定义了线程执行完毕后的返回结果。线程定义完成后如果要进行多线程的启动依然需要通过Thread类实现,所以此时可以通过java.util.concurrent.FutureTask类实现Callable接口与Thread类之间的联系,并且可以可用FutureTask类获取Callable接口中call()方法的返回值,此时采用的类结构如图。
清楚了FutureTask类结构后,下面来研究一下FutureTask类的常用方法,如表。
通过FutureTask类继承结构可以发现它是Runnable接口的子类,并且FutureTask类可以接收Callable接口实例,这样依然可以利用Thread类来实现多线程的启动,而如果要想接收返回结果,则利用Future接口中的get()方法即可。
范例:启动线程并获取Callable返回值
package cn.kuiba.util;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
class MyThread implements Callable<String> { //定义线程主体类
@Override
public String call() throws Exception { //线程执行方法
for (int x = 0; x < 10; x++) {
System.out.println("********线程执行、x="+x);
}
return "www.kuiba.cn"; //返回结果
}
}
public class Main {
public static void main(String args[])throws Exception{
//将Callable实例包装在FutureTask类中,这样就可以与Runnable接口关联
FutureTask<String>task=new FutureTask<>(new MyThread());
new Thread(task).start(); //线程启动
System.out.println("【线程返回数据】"+task.get()); //获取返回结果
}
}
程序执行结果:
********线程执行、x=0
********线程执行、x=1
********线程执行、x=2
********线程执行、x=3
********线程执行、x=4
********线程执行、x=5
********线程执行、x=6
********线程执行、x=7
********线程执行、x=8
********线程执行、x=9
【线程返回数据】www.kuiba.cn
本程序将Callable接口的子类利用FutureTask类对象进行包装,由于FutureTask是利用Runnable接口的子类,所以可以利用Thread类的start()方法启动多线程,当线程执行完毕后,可以利用Future接口中的get()方法返回线程的执行结果。
提示:Runnable与Callable的区别。
- Runnable是在JDK1.0的时候提出的多线程的实现接口,而Callable是在JDK1.5之后提出的。
- java.lang.Runnable接口中只提供有一个run()方法,并且没有返回值。
- java.util.concurrent.Callable接口提供有call()方法,可以有返回值(通过Future接口获取)。
2.5多线程运行状态
要想实现多线程,必须在主线程中创建新的线程对象。任意线程一般具有5种基本状态:创建、就绪、运行、阻塞、终止。线程状态的转移与方法之间的关系如图。
1. 创建状态
在程序种用构造方法创建一个线程对象后,新的线程对象便处于新建状态,此时,他已经有了相应的内存空间和其他资源,但还处于不可运行的状态。新建一个线程对象可采用Thread类的构造方法来实现,例如,Thread thread=new Thread();。
2.就绪状态
新建线程对象后,调用该线程的start()方法就可以启动线程。当线程启动时,线程就进入就绪状态。此时将进入线程队列排队,等待CPU调度服务,这表明它已经具备了运行条件。
3.运行状态
当就绪状态的线程被调用并获得处理器资源时,线程就进入了运行状态。此时将自动调用该线程对象的run()方法,run()方法定义了该线程的操作和功能。
4.阻塞状态
一个正在运行的线程在某些特殊情况下,如被人为挂起或需要进行耗时的输入/输出操作时,将让出CPU并暂时中止自己的运行,进入阻塞状态。在可运行状态下,如果调用sleep()、suspend()、wait()等方法,线程都将进入阻塞状态。阻塞时,线程不能进入排队队列,只有当引起阻塞的原因被消除后,线程才可以转入就绪状态。
5.终止状态
当线程体中的run()方法运行结束后,线程即处于终止状态,处于终止状态的线程不具有继续运行的能力。