实现多线程技术
继承Thread方法
Java虚拟机允许应用程序同时运行多个执行线程,每个线程都有独立的栈空间,并共享一份堆内存。一个任务对象如果要能够并发执行,就需要继承Thread类。
在继承Thread类时,需要重写run()方法,在run()方法中构写的就是一条新的执行路径,也就是所需要并发执行的代码。需要注意的一点,当继承Thread的子类对象任务创建之后,并不是通过调用run()来触发运行,而是通过调用start()来启动任务。下面通过一段代码来观察。
import javax.swing.plaf.TableHeaderUI;
import java.io.*;
import java.util.concurrent.LinkedTransferQueue;
public class Demo {
public static void main(String[] args) throws IOException, InterruptedException {
//创建一个名为mt的任务对象
MyThread mt = new MyThread();
//启动mt任务
mt.start();
//在main方法中打印10-15
for(int j=10;j<15;j++){
//打印 当前线程的名称:第j次打印
System.out.println(Thread.currentThread().getName()+":第"+j+"次打印");
}
}
}
//创建一个名为MyThread的类,并继承Thread
class MyThread extends Thread {
//重写run()方法,run()中的方法体就是一条新的执行路径
@Override
public void run() {
for(int i=0;i<5;i++){
//打印 当前线程名:第i次打印
System.out.println(Thread.currentThread().getName()+":第"+i+"次打印");
}
}
}
然后我们来观察这段代码的输出结果
main:第10次打印
Thread-0:第0次打印
main:第11次打印
Thread-0:第1次打印
main:第12次打印
Thread-0:第2次打印
main:第13次打印
Thread-0:第3次打印
main:第14次打印
Thread-0:第4次打印
这里"main"是主线程的名称,"Thread-0"是我们创建的mt任务的线程名称。可以看见,这里main方法中的任务和mt中的任务是并发执行的。然后我们再重新运行这段代码几次,会发现有不一样的运行结果。
main:第10次打印
Thread-0:第0次打印
main:第11次打印
main:第12次打印
main:第13次打印
Thread-0:第1次打印
main:第14次打印
Thread-0:第2次打印
Thread-0:第3次打印
Thread-0:第4次打印
这是因为在java中,线程的调度机制是抢占式的。优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性)。所以,当"main"线程和Thread-0"线程都在运行时,哪个先抢占到CPU的使用权,哪个线程先执行。所以,当我们执行这段代码的时候,输入的结果不是唯一的。
实现Runable接口
除了继承Thread类外,还有一种常用的实现多线程的方法,那就是实现Runable接口。这个方法的实现方式与继承Thread类的方法类似,也是重写run()方法。然后调用Thread.start()启动线程。下面也是用一段代码来展示:
public class Demo {
public static void main(String[] args) throws IOException, InterruptedException {
//创建一个名为mt的任务对象
MyThread myThread = new MyThread();
//将实现Runable类接口的对象以Thread来实现
Thread mt = new Thread(myThread);
//启动线程
mt.start();
//在main方法中打印10-15
for(int j=10;j<15;j++){
//打印 当前线程的名称:第j次打印
System.out.println(Thread.currentThread().getName()+":第"+j+"次打印");
}
}
}
//创建一个名为MyThread的类,并实现Runable接口
class MyThread implements Runnable {
//重写run()方法,run()中的方法体就是一条新的执行路径
@Override
public void run() {
for(int i=0;i<5;i++){
//打印 当前线程名:第i次打印
System.out.println(Thread.currentThread().getName()+":第"+i+"次打印");
}
}
}
Thread-0:第0次打印
main:第10次打印
Thread-0:第1次打印
main:第11次打印
Thread-0:第2次打印
main:第12次打印
Thread-0:第3次打印
main:第13次打印
Thread-0:第4次打印
main:第14次打印
上面这两个实现多线程的方法很相似,因为继承父类Thread来实现多线程的方法,在父类Thread中已经实现Runable接口,子类只需要重写其中的run()方法即可。在实际代码编写中,第二种方法用的更多。相比继承父类Thread来实现多线程来说,直接实现Runable接口有更大的优势:
- 通过创建任务,然后给线程分配的方式来实现多线程,更适合多个线程同时执行 的情况。
- 可以避免单继承所带来的局限性(java中的类只能单继承,但是接口可以多实现)
- 任务与线程是分离的,提高了程序的健壮性。
- 在线程池中,线程池接收实现Runable类的任务对象,不接收Tread对象的线程。
Callable
除了上面两个创建线程的方式之外,还有一种实现Callable接口来实现多线程的方法。Callable使用步骤:
- 编写类实现Callable接口 , 实现call方法
class XXX implements Callable<T> {
@Override
public <T> call() throws Exception {
return T;
}
}
- 创建FutureTask对象 , 并传入第一步编写的Callable类对象
FutureTask<Integer> future = new FutureTask<>(callable);
- 通过Thread,启动线程
new Thread(future).start();
Runnable 与 Callable的相同点
- 都是接口
- 都可以编写多线程程序
- 都采用Thread.start()启动线程
Runnable 与 Callable的不同点
- Runnable没有返回值;Callable可以返回执行结果
- Callable接口的call()允许抛出异常;Runnable的run()不能抛出Callable获取返回值
- Callalble接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。