首页 > 编程笔记 > Java笔记 阅读:4

Java多线程的3种实现方式(非常详细,附带实例)

Java 语言提供了 3 种实现多线程的方式,继承 Thread 类、实现 Runnable 接口、使用 Callable 接口和 Future 接口。

Java继承Thread类实现多线程

Java 提供了 Thread 类,代表线程,它位于 java.lang 包中,开发人员可以通过继承 Thread 类来创建并启动多线程,具体步骤如下:
启动一个新线程时,需要创建一个 Thread 类的实例,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 类还有一些常用方法,如下表所示:

表 2 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

程序中声明了一个 MyThread 类,继承 Thread 类,并且在类中重写了 run() 方法,方法的功能是循环打印小于 10 的奇数,其中 currentThread() 方法是 Thread 类的静态方法,调用该方法返回的是当前正在执行的线程对象的引用。

Demo 类在 main() 方法中创建了两个 MyThread 类的实例对象,分别调用实例对象的 start() 方法启动两个线程,两个线程都运行成功。

注意,如果 start() 方法调用一个已经启动的线程,程序会抛出 IllegalThreadStateException 异常。

Java实现Runnable接口实现多线程

Runnable 是 Java 中用于实现线程的接口,从理论上来讲,任何实现线程功能的类都必须实现该接口。前面讲到的继承 Thread 类的方式创建多线程,实际上就是因为 Thread 类实现了 Runnable 接口,所以它的子类才具有了线程的功能。

但是,Java 只支持单继承,一个类只能有一个父类,当一个类继承 Thread 类之后就不能再继承其他类,因此可以用实现 Runnable 接口的方式创建多线程,这种创建线程的方式更具有灵活性,同时可令用户线程能够具有其他类的一些特性,所以这种方法是经常使用的。

通过实现 Runnable 接口创建并启动多线程的步骤如下:
接下来,通过案例来演示如何通过实现 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

程序中,MyThread 类实现了 Runnable 接口并且重写了 run() 方法,方法的功能是循环打印小于 10 的奇数。Demo 类在 main() 方法中以 MyThread 类的实例分别创建并开启两个线程对象,调用 Thread(Runnable target, String name) 构造方法的目的是指定线程的名称“线程1”和“线程2”。

Java通过Callable接口和Future接口实现多线程

前文创建多线程的两种方式都有一个缺陷,在执行完任务之后无法获取线程的执行结果,如果想要获取执行结果,就必须通过共享变量或者使用线程通信的方式来达到,这样使用起来就比较麻烦。

于是,JDK 5.0 后 Java 便提供了 Callable 接口来解决这个问题,该接口内有一个 call() 方法,这个方法是线程执行体,有返回值且可以抛出异常。

通过实现 Callable 接口创建并启动多线程的步骤如下:
  1. 定义 Callable 接口实现类,指定返回值的类型,并重写 call() 方法。
  2. 创建 Callable 实现类的实例。
  3. 使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了 Callable 对象的 call() 方法的返回值。
  4. 将 FutureTask 类的实例注册进入 Thread 类中并启动线程。
  5. 采用 FutureTask<V> 中的 get() 方法获取自定义线程的返回值。

Callable 接口不是 Runnable 接口的子接口,所以不能直接作为 Thread 类构造方法的参数,而且 call() 方法有返回值,是被调用者。JDK 5.0 中提供了 Future 接口,该接口有一个 FutureTask 实现类,该类实现了 Runnable 接口,封装了 Callable 对象的 call() 方法的返回值,所以该类可以作为参数传入 Thread 类中。

接下来,先了解一下 Future 接口的常用方法,如下表所示:

表: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 = 线程执行完毕!!!

程序中 MyThread 类实现了 Callable 接口,指定了返回值的类型并且重写了 call() 方法。该方法主要是用于打印倒计时的时间。main() 方法中执行 15 次循环,并且在循环的过程中启动子线程并获取子线程的返回值。

反复执行实例程序,会发现有一个规律:“result = 线程执行完毕!!!”一直都是在最后输出,而“主线程循环执行完毕”输出的位置则不固定,有时候会在子线程循环前,有时候会在子线程循环后,有时候也会在子线程循环中。

之所以会出现这种现象,是因为通过 get() 方法获取子线程的返回值时,子线程的方法没有执行完毕,所以 get() 方法就会阻塞,当子线程中的 call() 方法执行完毕,get() 方法才能取到返回值。

3种实现多线程方式的对比

前面讲解了创建多线程的 3 种方式,这 3 种方式各有优缺点,具体如下表所示。

表:3 种实现多线程方式的对比
实现方式 优点 缺点
继承 Thread 类 程序代码简单
使用 run() 方法可以直接调用线程的其他方法
只能继承 Thread 类
不能实现资源共享
实现 Runnable 接口 符合面向对象的设计思想
便于继承其他的类
能实现资源共享
编程比较复杂
使用 Callable 接口和 Future 接口 便于继承其他的类
有返回值,可以抛异常
编程比较复杂

上表列出了 3 种创建多线程方式的优点和缺点,想要代码简洁就采用第 1 种方式,想要实现资源共享就采用第 2 种方式,想要有返回值并且能抛异常就采用第 3 种方式。

相关文章