在上一篇文章中,我们学习了操作系统中线程的基本概念。那么在 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 方法实现的;
好了,这次就到这里,下次再见!