导语
Java是一门为数不多的多线程支持的编程语言。
主要内容
- 掌握Java中三种多线程的实现方式
具体内容
如果想在Java之中实现多线程有两种途径:
- 继承Thread类。
- 实现Runnable接口(Callable接口)
继承Thread类
Thread类是一个支持多线程的功能类,只要有一个子类它就可以实现多线程的支持。
// 线程操作主类
public class MyThread extends Thread { // 这就是一个多线程的操作类
}
public class TestDemo { // 主类
public static void main(String args[]) {
}
}
所有程序的起点是main()方法,但是所有线程也一定要有一个自己的起点,那么这个起点就是run()方法,也就是说在多线程的每个主体类之中都必须覆写Thread类中所提供的run()方法。
public void run() {}
这个方法上没有返回值,线程一旦开始就要一直执行,不能够返回内容。
// 线程操作主类
public class MyThread extends Thread { // 这就是一个多线程的操作类
private String name;
public MyThread(String name) { // 定义构造方法
this.name = name;
}
@Override
public void run() { // 覆写run()方法,作炎线程的主体操作方法
for(int i = 0; i < 200; i++) {
System.out.println(this.name + "-->" + i);
}
}
}
public class TestDemo { // 主类
public static void main(String args[]) {
MyThread mt1 = new MyThread("线程A");
MyThread mt2 = new MyThread("线程B");
MyThread mt3 = new MyThread("线程C");
mt1.run();
mt2.run();
mt3.run();
}
}
输出结果
线程A-->0
线程A-->1
...
线程A-->198
线程A-->199
线程B-->0
线程B-->1
...
线程B-->198
线程B-->199
线程C-->0
线程C-->1
...
线程C-->198
线程C-->199
本线程类的功能是进行循环的输出操作,所有的线程与进程是一样的,都必须轮流去抢占资源,所以多线程的执行应该是多个线程彼此交替执行,也就是说如果直接调用了run()方法,那么并不能够启动多线程,多线程的启动的唯一方法就是Thread类中的start()方法:public void start() (调用此方法执行的方法体是run()方法定义的)。
修改代码
public class TestDemo { // 主类
public static void main(String args[]) {
MyThread mt1 = new MyThread("线程A");
MyThread mt2 = new MyThread("线程B");
MyThread mt3 = new MyThread("线程C");
mt1.start();
mt2.start();
mt3.start();
}
}
输出结果
线程A-->0
线程A-->1
线程C-->0
线程B-->0
线程C-->1
线程B-->1
线程A-->2
...
此时每一个线程对象交替执行。
观察Thread的源代码
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
started = false;
try {
nativeCreate(this, stackSize, daemon);
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
private native static void nativeCreate(Thread t, long stackSize, boolean daemon);
但是每个线程只能执行一次start()方法,否则会抛出IllegalThreadStateException异常。
发现在start()方法里面要调用一个nativeCreate()方法,而且此方法的结构与抽象方法类似,使用了native声明,在Java的开发里面有一门技术称为JNI技术(Java Native Interface),这门技术的特点:使用Java调用本机操作系统提供的函数。但是这样的技术有一个缺点,不能够离开特定的操作系统。
如果想要线程能够执行,需要操作系统来进行资源分配,所以此操作严格来讲主要是由JVM负责根据不同的操作系统而实现的。
即:使用Thread类的start()方法不仅仅要启动多线程的执行代码,还要去根据不同的操作系统进行资源分配。
实现Runnable接口
虽然Thread类可以实现多线程的主体类定义,但是它有一个问题,Java具有单继承局限,正因为如此在任何情况下针对于类的继承都应该是回避的问题,那么多线程也一样,为了解决单继承的限制,在Java里面专门提供了Runnable接口,此接口定义如下。
@FunctionalInterface
public interface Runnable {
public void run();
}
那么只需要让一个类实现Runnable接口即可,并且也需要覆写run()方法。
public class MyThread implements Runnable { // 这就是一个多线程的操作类
private String name;
public MyThread(String name) { // 定义构造方法
this.name = name;
}
@Override
public void run() { // 覆写run()方法,作炎线程的主体操作方法
for(int i = 0; i < 200; i++) {
System.out.println(this.name + "-->" + i);
}
}
}
与继承Thread类相比,此时的MyThread类在结构上与之前 是没有区别的,但是有一点是有严重区别的,如果此时继承了Thread类,那么可以直接继承start()方法,但是如果实现的是Runnbale接口,并没有start()方法可以被继承。
不管何种情况下,如果想要启动多线程一定依靠Thread类完成,在Thread类里面定义有以下的构造方法:public Thread(Runnable target),接收的是Runnable接口对象。
启动多线程:
public class TestDemo { // 主类
public static void main(String args[]) {
MyThread mt1 = new MyThread("线程A");
MyThread mt2 = new MyThread("线程B");
MyThread mt3 = new MyThread("线程C");
new Thread(mt1).start();
new Thread(mt2).start();
new Thread(mt3).start();
}
}
此时就避免了单继承局限,那么也就是说在实际工作中使用Runnable接口是最合适的。
多线程两种实现方法的区别
通过讲解已经清楚了多线程的两种实现方式,那么这两种方式有哪些区别呢?
首先一定要明确的是,使用Runnable接口与Thread类相比,解决了单继承的定义局限,所以不管后面的区别与联系是什么,至少这 一点上就能发现,Runnable接口更优。
首先观察一下Thread类的定义。
public class Thread implements Runnable {}
发现Thread类实现了Runnable接口,那么这样一来程序就变为了以下的形式。
此时,整个的定义结构看起来非常像代理设计模式,如果是代理设计模式,客户端调用的代理类的方法也应该是接口里提供的方法,那么也应该是run()才对(当时技术不成熟)。
除了以上的联系之外,还有一点:使用Runnable接口可以比Thread类能够更好的描述数据共享这一概念。此时的数据共享指的是多个线程访问同一资源的操作。
范例:观察代码(每一个线程对象都必须通过start()启动)
public class MyThread extends Thread {
private int ticket = 10;
@Override
public void run() { // 覆写run()方法,作炎线程的主体操作方法
for(int i = 0; i < 100; i++) {
if(this.ticket > 0) {
System.out.println("卖票,ticket = " + this.ticket--);
}
}
}
}
public class TestDemo { // 主类
public static void main(String args[]) {
// 由于MyThread类有start()方法,所以每一个MyThread类对象就是一个线程对象,可以直接启动
MyThread mt1 = new MyThread();
MyThread mt2 = new MyThread();
MyThread mt3 = new MyThread();
mt1.start();
mt2.start();
mt3.start();
}
}
输出结果
卖票,ticket = 10
卖票,ticket = 10
卖票,ticket = 9
卖票,ticket = 10
...
本程序声明了三个MyThread类对象,并且分别调用了三次start()方法,启动线程对象。但是最终的结果发现每一个线程对象都在卖各自的10张票,因为此时产生了三个线程对象,此时并不存在有数据共享这一概念。
范例:利用Runnable来实现
public class MyThread implements Runnable {
private int ticket = 10;
@Override
public void run() { // 覆写run()方法,作炎线程的主体操作方法
for(int i = 0; i < 100; i++) {
if(this.ticket > 0) {
System.out.println("卖票,ticket = " + this.ticket--);
}
}
}
}
public class TestDemo { // 主类
public static void main(String args[]) {
MyThread mt = new MyThread();
new Thread(mt).start();
new Thread(mt).start();
new Thread(mt).start();
}
}
输出结果
卖票,ticket = 10
卖票,ticket = 9
卖票,ticket = 8
卖票,ticket = 7
...
此时也属于三个线程对象,可是唯一的区别是,这三个线程对象都直接占用了同一个MyThread类的对象引用,也就是说这三个线程对象都直接访问同一个数据资源。
Thread类与Runnable接口实现多线程的区别:
- Thread类是Runnable接口的子类,使用Runnable接口实现多线程可以避免单继承局限。
- Runnable接口实现的多线程可以比Thread类实现的多线程更加清楚的描述了数据共享的概念。
第三种实现方式(理解)
使用Runnable接口实现的多线程可以避免单继承局限,但是有一个问题,Runnable接口里面的run()方法,不能返回操作结果。为了解决这样的矛盾,提供了一个新的接口java.util.conurrent.Callable<V>接口。
@FunctionalInterface
public interface Callable<V> {
public V call() throws Exception;
}
call()方法执行完线程的主题功能之后可以返回一个结果,而返回结果的类型由Callable接口上的泛型来决定。
范例:定义一个线程主体类
class MyThread implements Callable<String> {
private int ticket = 10;
@Override
public String call() throws Exception {
for(int i = 0; i < 100; i++) {
if(this.ticket > 0) {
System.out.println("卖票,ticket = " + this.ticket--);
}
}
return "票已卖光!";
}
}
观察发现Thread类里面没有发现直接支持Callable接口的多线程应用。
从JDK1.5开始提供有java.util.concurrent.FutureTask<V>类。这个类主要是负责Callable接口对象操作的,这个接口的定义结构:
public class FutureTask<V> implements RunnableFuture<V> {}
public interface RunnableFuture<V> extends Runnable, Future<V> {}
在FutureTask类里面定义有如下的构造方法:
public FutureTask(Callable<V> callable) {}
接收Callable对象的目的只有一个,那么就是取得call()方法的返回结果。
启动线程的代码如下。
public class TestDemo {
public static void main(String args[]) throws Exception {
MyThread mt = new MyThread();
FutureTask<String> task = new FutureTask(mt); // 目的是为了取得call()返回结果,记得设置返回结果类型
// FutureTask是Runnable接口子类,所以可以使用Thread类的构造来接收task对象
new Thread(task).start(); // 启动多线程
// 多线程执行完毕后可以取得内容,依靠FutureTask的父接口Future中的get()方法完成
System.out.println("线程的返回结果:" + task.get();
}
}
输出结果
卖票,ticket = 10
卖票,ticket = 9
卖票,ticket = 8
...
卖票,ticket = 1
票已卖光!
最麻烦的问题在于需要接收返回值信息,并且又要与原始的多线程(Thread类)的实现靠拢。
总结
- 对于多线程的实现,重点在于Runnable接口与Thread类启动的配合上。
- 对于JDK1.5新特性,Callable区别就在于返回值的实现。