Java多线程的3种实现方式(非常详细,附带实例)
Java继承Thread类实现多线程
Java 提供了 Thread 类,代表线程,它位于 java.lang 包中,开发人员可以通过继承 Thread 类来创建并启动多线程,具体步骤如下:- 从 Thread 类派生出一个子类,并且在子类中重写 run() 方法;
- 用这个子类创建一个实例对象;
- 调用对象的 start() 方法启动线程。
启动一个新线程时,需要创建一个 Thread 类的实例,Thread 类的常用构造方法如下表所示。
构造方法 | 方法描述 |
---|---|
public Thread() | 创建新的 Thread 对象,自动生成的线程名称为 Thread-n,其中 n 为整数 |
public Thread(String name) | 创建新的 Thread 对象,name 是新线程的名称 |
public Thread(Runnable target) | 创建新的 Thread 对象,其中 target 是 run() 方法被调用时的对象 |
public Thread(Runnable target, String name) | 创建新的 Thread 对象,其中 target 是 run() 方法被调用时的对象,name 是新线程的名称 |
表中列出了 Thread 类的常用构造方法,创建线程实例时需要使用这些构造方法,线程中真正的功能代码写在这个类的 run() 方法中。当一个类继承 Thread 类之后,要重写父类的 run() 方法。
另外,Thread 类还有一些常用方法,如下表所示:
方法 | 方法描述 |
---|---|
String getName() | 返回该线程的名称 |
Thread.State getState() | 返回该线程的状态 |
boolean isAlive() | 判断该线程是不是处于活跃状态 |
void setName(String name) | 更改线程的名字,使其与参数name保持一致 |
void start() | 开始执行线程,Java虚拟机调用该线程里面的run()方法 |
static void sleep(long millis) | 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器与调度程序精度和准确性的影响 |
static Thread currentThread() | 返回当前正在运行的线程的对象的引用 |
接下来,通过案例来演示使用继承 Thread 类的方式创建多线程:
public class Demo { public static void main(String[] args) { // 创建MyThread实例对象 MyThread myThread1 = new MyThread(); // 开启线程 MyThread myThread2 = new MyThread(); myThread1.start(); myThread2.start(); } } class MyThread extends Thread { // 重写run()方法 public void run() { for (int i = 0; i < 10; i++) { if (i % 2 != 0) { System.out.println(Thread.currentThread().getName() + ":" + i); } } } }程序的运行结果如下:
Thread-0:1
Thread-1:1
Thread-0:3
Thread-1:3
Thread-0:5
Thread-0:7
Thread-0:9
Thread-1:5
Thread-1:7
Thread-1:9
Demo 类在 main() 方法中创建了两个 MyThread 类的实例对象,分别调用实例对象的 start() 方法启动两个线程,两个线程都运行成功。
注意,如果 start() 方法调用一个已经启动的线程,程序会抛出 IllegalThreadStateException 异常。
Java实现Runnable接口实现多线程
Runnable 是 Java 中用于实现线程的接口,从理论上来讲,任何实现线程功能的类都必须实现该接口。前面讲到的继承 Thread 类的方式创建多线程,实际上就是因为 Thread 类实现了 Runnable 接口,所以它的子类才具有了线程的功能。但是,Java 只支持单继承,一个类只能有一个父类,当一个类继承 Thread 类之后就不能再继承其他类,因此可以用实现 Runnable 接口的方式创建多线程,这种创建线程的方式更具有灵活性,同时可令用户线程能够具有其他类的一些特性,所以这种方法是经常使用的。
通过实现 Runnable 接口创建并启动多线程的步骤如下:
- 定义 Runnable 接口实现类,并重写 run() 方法;
- 创建 Runnable 接口实现类的实例对象,并将该实例对象传递给 Thread 类的一个构造方法,该实例对象提供线程体 run() 方法;
- 调用实例对象的 start() 方法启动线程。
接下来,通过案例来演示如何通过实现 Runnable 接口的方式创建多线程。
public class Demo { public static void main(String[] args) { // 创建myThread对象 MyThread myThread = new MyThread(); // 启动线程 new Thread(myThread, "线程1").start(); new Thread(myThread, "线程2").start(); } } class MyThread implements Runnable { // 重写run()方法 public void run() { for (int i = 0; i < 10; i++) { if (i % 2 != 0) { System.out.println(Thread.currentThread().getName() + ":" + i); } } } }程序的运行结果如下:
线程1:1
线程1:3
线程2:1
线程1:5
线程2:3
线程1:7
线程2:5
线程1:9
线程2:7
线程2:9
Java通过Callable接口和Future接口实现多线程
前文创建多线程的两种方式都有一个缺陷,在执行完任务之后无法获取线程的执行结果,如果想要获取执行结果,就必须通过共享变量或者使用线程通信的方式来达到,这样使用起来就比较麻烦。于是,JDK 5.0 后 Java 便提供了 Callable 接口来解决这个问题,该接口内有一个 call() 方法,这个方法是线程执行体,有返回值且可以抛出异常。
通过实现 Callable 接口创建并启动多线程的步骤如下:
- 定义 Callable 接口实现类,指定返回值的类型,并重写 call() 方法。
- 创建 Callable 实现类的实例。
- 使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了 Callable 对象的 call() 方法的返回值。
- 将 FutureTask 类的实例注册进入 Thread 类中并启动线程。
- 采用 FutureTask<V> 中的 get() 方法获取自定义线程的返回值。
Callable 接口不是 Runnable 接口的子接口,所以不能直接作为 Thread 类构造方法的参数,而且 call() 方法有返回值,是被调用者。JDK 5.0 中提供了 Future 接口,该接口有一个 FutureTask 实现类,该类实现了 Runnable 接口,封装了 Callable 对象的 call() 方法的返回值,所以该类可以作为参数传入 Thread 类中。
接下来,先了解一下 Future 接口的常用方法,如下表所示:
方法 | 方法描述 |
---|---|
boolean cancel(boolean b) | 试图取消对该任务的执行 |
V get() | 如有必要,等待计算完成,然后获取其结果 |
V get(long timeout, TimeUnit unit) | 如有必要,最多等待使计算完成所用时间之后,获取其结果(若结果可用) |
boolean isCancelled() | 如果在任务正常完成前将其取消,则返回 true |
boolean isDone() | 如果任务已完成,则返回 true |
接下来,通过案例来演示如何通过 Callable 接口和 Future 接口创建多线程:
import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; public class Demo { public static void main(String[] args) { Callable<String> callable = new MyThread(); // 创建Callable对象 // 使用FutureTask来包装Callable对象 FutureTask<String> futureTask = new FutureTask<String>(callable); for (int i = 0; i < 15; i++) { System.out.println(Thread.currentThread().getName() + ":" + i); if (i == 1) { // FutureTask对象作为Thread对象的参数创建新的线程 Thread thread = new Thread(futureTask); thread.start(); // 启动线程 } } System.out.println("主线程循环执行完毕"); try { // 取得新创建线程中的call()方法返回值 String result = futureTask.get(); System.out.println("result = " + result); } catch (Exception e) { e.printStackTrace(); } } } class MyThread implements Callable<String> { public String call() { for (int i = 10; i > 0; i--) { System.out.println(Thread.currentThread().getName() + "倒计时:" + i); } return "线程执行完毕!!!"; } }程序的第 1 次运行结果如下:
main:0
main:1
main:2
main:3
main:4
main:5
main:6
main:7
main:8
main:9
main:10
main:11
main:12
main:13
main:14
主线程循环执行完毕
Thread-0倒计时:10
Thread-0倒计时:9
Thread-0倒计时:8
Thread-0倒计时:7
Thread-0倒计时:6
Thread-0倒计时:5
Thread-0倒计时:4
Thread-0倒计时:3
Thread-0倒计时:2
Thread-0倒计时:1
result = 线程执行完毕!!!
程序的第 2 次运行结果如下:
main:0
main:1
main:2
main:3
main:4
main:5
main:6
main:7
main:8
main:9
Thread-0倒计时:10
main:10
Thread-0倒计时:9
main:11
Thread-0倒计时:8
main:12
Thread-0倒计时:7
main:13
main:14
主线程循环执行完毕
Thread-0倒计时:6
Thread-0倒计时:5
Thread-0倒计时:4
Thread-0倒计时:3
Thread-0倒计时:2
Thread-0倒计时:1
result = 线程执行完毕!!!
反复执行实例程序,会发现有一个规律:“result = 线程执行完毕!!!”一直都是在最后输出,而“主线程循环执行完毕”输出的位置则不固定,有时候会在子线程循环前,有时候会在子线程循环后,有时候也会在子线程循环中。
之所以会出现这种现象,是因为通过 get() 方法获取子线程的返回值时,子线程的方法没有执行完毕,所以 get() 方法就会阻塞,当子线程中的 call() 方法执行完毕,get() 方法才能取到返回值。
3种实现多线程方式的对比
前面讲解了创建多线程的 3 种方式,这 3 种方式各有优缺点,具体如下表所示。实现方式 | 优点 | 缺点 |
---|---|---|
继承 Thread 类 |
程序代码简单 使用 run() 方法可以直接调用线程的其他方法 |
只能继承 Thread 类 不能实现资源共享 |
实现 Runnable 接口 |
符合面向对象的设计思想 便于继承其他的类 能实现资源共享 |
编程比较复杂 |
使用 Callable 接口和 Future 接口 |
便于继承其他的类 有返回值,可以抛异常 |
编程比较复杂 |
上表列出了 3 种创建多线程方式的优点和缺点,想要代码简洁就采用第 1 种方式,想要实现资源共享就采用第 2 种方式,想要有返回值并且能抛异常就采用第 3 种方式。