Java 并发编程:本质上只有一种创建线程的方法

开发
在上一篇文章中,我们学习了操作系统中线程的基本概念。那么在 Java 中,我们如何创建和使用线程呢?

上一篇文章中,我们学习了操作系统中线程的基本概念。那么在 Java 中,我们如何创建和使用线程呢?首先请思考一个问题。创建线程有多少种方法呢?大多数人会说有 2 种、3 种或 4 种。很少有人会说只有 1 种。让我们看看他们实际指的是什么。最常见的答案是两种创建线程的方法。让我们先看看这两种线程创建方法的代码。

Thread 类和 Runnable 接口

(1) 继承 Thread 类:第一种是继承 Thread 类并重写 run() 方法:

class SayHelloThread extends Thread {
    public void run() {
        System.out.println("hello world");
    }
}

public class ThreadJavaApp {
    public static void main(String[] args) {
        SayHelloThread sayHelloThread = new SayHelloThread();
        sayHelloThread.start();
    }
}

只有在主线程中创建 MyThread 的实例并调用 start() 方法,线程才会启动。

(2) 实现 Runnable 接口:接下来看看 Runnable 接口:

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

实现 Runnable 接口的 run() 方法,在这个方法里可以定义相应的业务逻辑,不过我们还是需要通过 Thread 类来启动线程。

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Hello Runnable");
    }
}

public class RunnableDemo {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        new Thread(myRunnable).start();
    }
}

从 Runnable 接口的定义中可以看出,Runnable 是一个函数式接口(JDK 1.8 及以上),这意味着我们可以使用 Java 8 的函数式编程来简化代码:

public class RunnableDemo {
    public static void main(String[] args) {
        // Java 8 函数式编程,可以省略 MyThread 类的定义
        new Thread(() -> {
            System.out.println("Lambda 表达式实现 Runnable");
        }).start();
    }
}

Callable、Future 和 FutureTask

一般来说,我们使用 Runnable 和 Thread 来创建一个新线程。然而,它们有一个缺点,即 run 方法没有返回值。有时我们希望启动一个线程来执行任务,并且在任务完成后有一个返回值。JDK 为我们提供了 Callable 接口来解决这个问题。

(1) Callable 接口:Callable 与 Runnable 类似,它也是一个只有一个抽象方法的函数式接口。不同之处在于 Callable 提供的方法有返回值并支持泛型。

@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

那么 Callable 通常如何使用呢?它是否与 Runnable 接口一样,传入 Thread 类呢?让我们查看 JDK8 的 Java API,发现没有使用 Callable 作为参数的构造方法。

(2) Future 接口和 FutureTask 类:实际上它提供了 FutureTask 类来完成有返回值的异步计算。FutureTask 实现了 RunnableFuture 接口,而 RunnableFuture 接口同时继承了 Runnable 接口和 Future 接口,因此可以传入 Thread(Runable target)。

public interface RunnableFuture<V> extends Runnable, Future<V> {
  /**
     * Sets this Future to the result of its computation,
     * unless it has been cancelled.
     */
    void run();
}

Future 接口只有几个简单的方法:

public abstract interface Future<V> {
    public abstract boolean cancel(boolean paramBoolean);
    public abstract boolean isCancelled();
    public abstract boolean isDone();
    public abstract V get() throws InterruptedException, ExecutionException;
    public abstract V get(long paramLong, TimeUnit paramTimeUnit)
            throws InterruptedException, ExecutionException, TimeoutException;
}

每个方法的功能如下:

  • get():等待计算完成并返回结果。
  • get(long paramLong, TimeUnit paramTimeUnit):等待设定的时间。如果在设定时间内计算完成,则返回结果,否则抛出 TimeoutException。
  • isDone:如果任务完成,则返回 true。完成可能是由于正常终止、异常或取消。在所有这些情况下,方法都将返回 true。
  • isCancelled:如果此任务在正常完成之前被取消,则返回 true。
  • cancel:尝试取消线程的执行。请注意,这是尝试取消,不一定能成功取消。因为任务可能已经完成、被取消或由于其他一些因素而无法取消,所以取消可能会失败。布尔类型的返回值表示取消是否成功。参数 paramBoolean 表示是否通过中断线程来取消线程执行。

有时使用 Callable 而不是 Runnable 是为了能有取消任务的能力。如果使用 Future 只是为了可以取消任务但不提返回结果可以声明 Future<? >的类型,并将底层任务的结果返回为 null。

你可能会问,为什么要有 FutureTask 类呢?前面说过 Future 只是一个接口,其方法 cancel、get、isDone 等如果自己实现会非常复杂。因此 JDK 为我们提供了 FutureTask 类供我们直接使用。

FutureTask 需要与 Callable 结合使用来完成有返回值的异步计算,这里看一个其使用的简单示例:

class MyCallable implements Callable<Integer> {
    /**
     * 计算 1 到 4 的总和
     * @return
     */
    @Override
    public Integer call() {
        int res = 0;
        for (int i = 0; i < 5; i++) {
            res += i;
        }
        return res;
    }
}

publicclass CallableDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 1. 生成 MyCallable 的实例
        MyCallable myCallable = new MyCallable();
        // 2. 通过 myCallable 创建 FutureTask 对象
        FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
        // 3. 通过 FutureTask 创建 Thread 对象
        Thread t = new Thread(futureTask);
        // 4. 启动线程
        t.start();
        // 5. 获取计算结果
        Integer res = futureTask.get();
        System.out.println(res);
    }
}

输出:

10

为什么只有一种实现线程的方法?

我相信你对这个问题基本上有了答案。无论你是实现 Runnable 接口、实现 Callable 接口还是直接继承 Thread 类来创建线程,最终都是创建一个 Thread 实例来启动线程,即 new Thread(),只是创建的形式不同而已!

实际上,线程不仅可以通过上述形式创建,还可以通过内置的工具类(如线程池)来创建,后续文章将单独介绍。

实现 Runnable 接口优于继承 Thread 类

要实现一个没有返回值的线程类,你可以继承 Thread 类或实现 Runnable 接口,它们之间有什么优缺点呢?

(1) Thread 类的优点:

  • 简单直观:由于继承关系,代码结构相对简单易懂。
  • 线程控制:可以直接使用 Thread 类的方法来控制线程的状态,如启动、暂停、停止等。

(2) Thread 类的缺点:

  • 单继承限制:由于 Java 不支持多重继承,使用 Thread 类限制了类的扩展。
  • 代码耦合:线程类和线程执行逻辑紧密耦合,不利于代码复用和维护。

(3) Runnable 接口的优点:

  • 更好的代码复用:由于它是一个接口,可以将线程的执行逻辑与其他类分离,以实现代码复用。
  • 灵活性:可以同时实现多个接口,避免单继承的限制。
  • 更好的可扩展性:接口使得在不影响现有代码的情况下扩展线程功能变得容易。即面向接口编程的原则。

(4) Runnable 接口的缺点:

  • 代码稍微复杂一些:需要创建一个实现 Runnable 接口的类并实现 run() 方法,然后由 Thread 类驱动。
  • 没有线程控制方法:不能直接使用 Thread 类的线程控制方法,需要通过 Thread 对象调用它们。

所以,综合考虑,通常建议使用 Runnable 接口的实现来创建线程,以获得更好的代码可复用性和可扩展性。

Thread 的 start() 方法

在程序中调用 start() 方法后,虚拟机首先为我们创建一个线程,然后等待直到这个线程获得时间片,才会调用 run() 方法执行具体逻辑。

请注意,start() 方法不能多次调用。第一次调用 start() 方法后,再次调用会抛出 IllegalThreadStateException 异常。

你可以简单看一下 start() 方法的源代码。实际上,实际工作是由 start0() 完成的,它是一个本地方法,我添加了一些注释,以便你更容易理解。

public synchronized void start() {
    /**
     * 零状态值对应于状态 NEW。
     */
    if (threadStatus!= 0)
        thrownew IllegalThreadStateException();
    group.add(this); // 将其所属的线程组加上该线程
    boolean started = false;
    try {
        start0(); // 本地方法调用实际创建线程的底层方法。
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadstartFailed(this);
            }
        } catch (Throwable ignore) {
            /* 什么也不做。如果 start0 抛出了 Throwable,那么
             * 它将在调用栈中向上传递 */
        }
    }
}

ThreadGroup 的概念将在后续文章中介绍。你可以在这里忽略它。

Thread 类的几个常用方法

这里我们简要提及 Thread 类的几个常用方法,先熟悉一下。后续文章将根据具体使用场景详细介绍:

  • currentThread():静态方法,返回当前正在执行的线程对象的引用;
  • sleep():静态方法,使当前线程睡眠指定的时间;
  • yield():表示当前线程愿意放弃对当前处理器的占用。请注意,即使当前线程调用了 yield() 方法,它仍然有可能继续运行;
  • join():使当前线程等待另一个线程完成执行后再继续,内部调用是通过 Object 类的 wait 方法实现的;

好了,这次就到这里,下次再见!

责任编辑:赵宁宁 来源: 程序猿技术充电站
相关推荐

2012-08-13 10:26:53

云计算云服务

2022-06-06 15:44:24

大数据数据分析思维模式

2022-11-22 11:18:38

Java虚拟线程

2020-12-14 06:43:02

并发编程JDK

2024-04-01 08:38:57

Spring@AspectAOP

2023-10-24 09:03:05

C++编程

2023-12-04 08:21:18

虚拟线程Tomcat

2010-12-06 16:57:13

FreeBSDLinux

2011-02-23 09:35:25

Eclipse远程调试

2011-12-29 13:31:15

Java

2015-10-14 17:54:01

容器虚拟机云服务

2023-07-18 18:10:04

2018-12-14 14:30:12

安全检测布式系测试

2024-12-30 08:20:29

程序并发任务线程

2015-03-13 11:23:21

编程编程超能力编程能力

2011-07-04 17:53:48

快速测试

2012-11-01 13:41:25

编程语言BasicPerl

2012-07-30 09:58:53

2023-09-27 08:01:14

数据推送事件

2010-01-25 15:09:17

C++语言
点赞
收藏

51CTO技术栈公众号