java学习线程篇之多线程的实现方式:继承Thread类、实现Runnable接口或Callable接口
1. 基本概念:进程与线程
在操作系统的定义中,进程是指一次程序的完整运行,这个运行的过程之中内存、处理器、IO等资源操作都要为这个进程进行服务。
Windows属于多进程的操作系统,那么问题就来了?每一个进程都需要资源的支持,那么这么多进程怎么分配资源呢?
在同一个时间段上,会有多进程轮流去抢占资源,但是在某一个时间点上,只会有一个进程进行。
线程是在进程基础上进一步的划分结果,即:一个进程上可以同时创建多个线程。
线程是比进程更快的处理单元,而且所占的资源也小,那么多线程的应用也是性能最高的应用。
线程的存在离不开进程。进程如果消失,线程一定会消失;但是线程消失,进程未必消失。
2. 实现多线程的方式
在java中有三种多线程(JDK 1.5之后增加了第三种)。
2.1 继承Thread类
Thread类是一个支持多线程的一个功能类,只要有一个子类就可以实现多线程的支持。
所有程序的起点是main方法,但是所有线程也一定要有一个自己的起点,那么在多线程的每个主体类之中都必须覆写Thread类中提供的run()方法。
public void run(){}
这个方法没有返回值,那么也就表示线程一旦开始就要一直执行,不能够返回内容。
class MyThead extends Thread { //这就是一个多线程的操作类
private String name;
public MyThead(String name) { //定义构造方法
this.name = name;
}
@Override
public void run(){ //覆写run方法,作为线程的主体操作方法
for (int x = 0; x <5; x++) {
System.out.println(this.name+"-->"+x);
}
}
}
public class Test{
public static void main(String[] args) {
MyThead myThead1=new MyThead("线程1");
MyThead myThead12=new MyThead("线程2");
MyThead myThead3=new MyThead("线程3");
myThead1.run();
myThead12.run();
myThead3.run();
}
}
以上代码的功能实现循环输出。所有的线程与进程一样,必须轮流的去抢占资源。所以多线程的执行是多个线程彼此交替执行,也就是说,如果直接调用了run方法,那么并不能够启动多线程。多线程唯一的启动方法就是Thread类中的start方法。
public void start() // 调用此方法实际上执行的是run方法

start()方法中调用本地的start0()方法,start0()中调用了run方法。


此时每一个线程对象交替执行,因此,要想启动线程必须调用start()方法。
疑问:为什么多线程启动不是调用run方法,而是调用start方法?
首先,在Thread方法中存在一个异常抛出
IllegalThreadStateException(),本方法里的异常并没有用try catch处理,也没有在start()方法中用throws声明,因为此异常属于RuntimeException的子类,属于选择性处理。如果某一个线程对象重复进行启动,就会抛出此异常。
调用start0()方法,且此方法的结构与抽象方法类似,使用了native声明,在Java的开发里面有一门技术称为JNI技术(Java native Interface),这门技术的特点:使用java调用本机操作系统提供的函数,这样的技术有一个缺点,不能够离开特定的操作系统。如果要想线程能够执行,需要操作系统来进行资源分配,所以此操作严格来讲主要是由JVM根据不同的操作系统实现的。
即:使用Thread类的start()方法不仅要启动多线程的执行代码,还要去根据不同的操作系统进行资源的分配。

2.2 实现Runnable接口
虽然Thread类可以实现多线程的主体类定义,但是它有一个问题,java具有单线程局限,正因如此针对于类的继承都应该是回避的问题,那么多线程也一样。为了解决单线程的限制,在java里面专门提供了Runnable接口,此接口定义如下:

与继承Thread类相比,实现Runnable接口的MyThread类在结构上与之前是没有区别的,但是有一点是有严重区别的,如果继承了Thread类,那么可以直接继承start()方法,但是实现Runnable接口是没有start()方法的。
不管何种情况下,如果要启动多线程一定要依靠Thread类完成。
在Thread类中有以下的构造方法:
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
接收Runnable接口对象,则可以利用其构造函数启动多线程。
class MyThead1 implements Runnable{ //这就是一个多线程的操作类
private String name;
public MyThead1(String name) { //定义构造方法
this.name = name;
}
@Override
public void run(){ //覆写run方法,作为线程的主体操作方法
for (int x = 0; x <200; x++) {
System.out.println(this.name+"-->"+x);
}
}
}
public class TestRun{
public static void main(String[] args) {
MyThead1 myThead1=new MyThead1("线程1");
MyThead1 myThead2=new MyThead1("线程2");
MyThead1 myThead3=new MyThead1("线程3");
new Thread(myThead1).start();
new Thread(myThead2).start();
new Thread(myThead3).start();
}
}
此时就避免了单继承局限,那么也就是说在实际工作中使用Runnable接口是最合适的。
2.3 多线程两种实现方式的区别
首先一定要明确的是,使用Runnable接口与Thread类相比,解决了单继承的定义局限,所以不管后面的区别与联系是什么,至少这一点就下了死定义----如果要使用一定使用Runnable接口。
首先,观察一下Thread类定义,

发现Thread类实现了Runnable接口。那么这样一来,
此时,整个的定义结构看起来非常像代理设计模式,如果是代理设计模式,客户端调用的代理类的方法也应该是接口里提供的方法,那么也应该是run()才对。
还有一点,使用Runnable接口可以比Thread类能够更好地描述出数据共享这一概念。此时的数据共享指的是多个线程访问同一资源的操作。
每一个线程对象都必须通过start()启动。
class MyThead extends Thread { //这就是一个多线程的操作类
private int price=10;
private String name;
public MyThead(String name) { //定义构造方法
this.name = name;
}
@Override
public void run(){ //覆写run方法,作为线程的主体操作方法
while (this.price>0) {
System.out.println(this.name+": 降价后的价格: "+this.price--);
}
}
}
public class TestThr{
public static void main(String[] args) {
MyThead myThead1=new MyThead("线程1");
MyThead myThead12=new MyThead("线程2");
MyThead myThead3=new MyThead("线程3");
myThead1.start();
myThead12.start();
myThead3.start();
}
}

三个线程对象各自占用内存空间,并不存在数据共享的这一概念。
class MyThead implements Runnable { //这就是一个多线程的操作类
private int price=10;
private String name;
public MyThead(String name) { //定义构造方法
this.name = name;
}
@Override
public void run(){ //覆写run方法,作为线程的主体操作方法
while (this.price>0) {
System.out.println(this.name+": 降价后的价格: "+this.price--);
}
}
}
public class TestThr{
public static void main(String[] args) {
MyThead myThead1=new MyThead("线程1");
MyThead myThead2=new MyThead("线程2");
MyThead myThead3=new MyThead("线程3");
//myThead1.start();
// myThead12.start();
//myThead3.start();
new Thread(myThead1).start();
new Thread(myThead2).start();
new Thread(myThead3).start();
}
}
但是在实现Runnable接口时,三个线程对象都直接占用同一个MyThread类的对象引用,也就是说这三个线程对象都直接访问同一个数据资源。
面试题:请解释Thread类和Runnable接口实现多线程的区别?(请解释多线程两种实现方式的区别?)
- Thread类是Runnable接口的子类,使用Runnable接口实现多线程可以避免单继承局限。
- Runnable接口实现的多线程可以比Thread类实现的多线程更加清楚的描述数据共享的概念。
面试题:请写出多线程两种实现操作。
把Thread类和Runnable接口实现方式的代码都写出来。
2.4 第三种实现方式(理解)
使用Runnable接口实现的多线程可以避免单继承局限,的确很好,但是Runnable接口里面的run()方法不能返回操作结果。为了解决这样的矛盾,提供了一个新的接口,即Callable接口。

Thread类里面并没有直接支持Callable接口的多线程应用。
从JDK1.5开始提供有public class FutureTask<V> implements RunnableFuture<V>这个类负责Callable接口对象操作。这个接口实现了RunnableFuture接口。
FutureTask类中的构造方法:
接收的目的只有一个,那么就是取得的call()方法的返回结果。
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
class MyThread implements Callable<String> {
private int price = 10;
@Override
public String call() throws Exception {
for (int x = 0; x <100; x++) {
System.out.println("价格: "+this.price--);
}
return "不能再降价了";
}
}
public class TestCal {
public static void main(String[] args) throws Exception {
MyThread myThead = new MyThread();
MyThread myThead1 = new MyThread();
FutureTask<String> task=new FutureTask<String>(myThead);
FutureTask<String> task1=new FutureTask<String>(myThead1);
new Thread(task).start();
new Thread(task1).start();
System.out.println(task.get());
System.out.println(task1.get());
}
}

最麻烦的问题在于需要接收返回值信息,并且要与原始的多线程的实现靠拢(Thread类)。
2.5 小结
- 对于多线程的实现,重点在于Runnable接口与Thread类启动的配合上;
- 对于JDK 1.5新特性,了解就行,知道区别在于返回结果上。