Java 并发编程 01:创建线程的三种方式

643 阅读4分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第2天,点击查看活动详情

简介

Java 中,创建线程的常见方式有以下三种:

  • 继承 Thread 类创建线程
  • 实现 Runnable 接口创建线程
  • 通过 FutureTask 类和 Callable 接口创建线程

团子注:有的人可能会觉得上述划分多余了,本质上只有一种通过 Thread 类创建线程的方式;也有人认为分少了,线程池和定时器等也能创建线程。个人认为这是一种便于理解的划分,不用太过认真,否则就变成研究“茴”字有多少种写法这种课题了。

继承 Thread 类创建线程

继承 Thread 类创建线程,主要步骤如下:

  • 继承 Thread
  • 重写 run() 方法

run() 方法体常被称作线程执行体,表示一个线程需要完成的任务和逻辑。

示例代码如下:

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("hello my thread!");
    }
}

public class ThreadTest {
    public static void main(String[] args) {
        Thread t = new MyThread();
        t.start();
        // hello my thread!
    }
}

如果线程执行体不需要复用,也可以采用匿名内部类的形式编写 Thread 类的子类,上述代码的等价形式如下:

public class ThreadTest {
    public static void main(String[] args) {
        Thread t = new Thread() {
            public void run() {
                System.out.println("hello my thread!");
            }
        };
        t.start();
        // hello my thread!
    }
}

当然,还可以利用 Java 8 特性,进一步简化成 Lambda 表达式的形式,上述代码的等价形式如下:

public class ThreadTest {
    public static void main(String[] args) {
        Thread t = new Thread(() -> System.out.println("hello my thread!"));
        t.start();
        // hello my thread!
    }
}

实现 Runnable 接口创建线程

Runnable 接口是一个函数式接口,位于 java.lang 包中,它的源码如下:

package java.lang;

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

实现 Runnable 接口创建线程,主要步骤如下:

  • 实现 Runnable 接口的 run() 方法
  • 使用 Thread 类创建线程,传入 Runnable 接口对象

下面的代码定义了 Runnable 接口的实现类,实现其 run() 方法,并给 Thread 类的构造函数传入了 Runnable 接口对象。

class MyThread implements Runnable{
    @Override
    public void run() {
        System.out.println("hello my thread!");
    }
}

public class ThreadTest {
    public static void main(String[] args) {
        Runnable runnable = new MyThread();
        Thread t = new Thread(runnable);
        t.start();
        // hello my thread!
    }
}

同样,也可以采用匿名内部类、Lambda 表达式的形式改写上述代码。

下面是用匿名内部类的形式实现的 Runnable 接口:

public class ThreadTest {
    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello my thread!");
            }
        });
        t.start();
        // hello my thread!
    }
}

下面的代码则采用了 Lambda 表达式,进一步简化代码:

public class ThreadTest {
    public static void main(String[] args) {
        Thread t = new Thread(() -> System.out.println("hello my thread!"));
        t.start();
        // hello my thread!
    }
}

通过 FutureTask 类和 Callable 接口创建线程

Callable 接口与 Runnable 接口都是函数式接口,它们的不同之处在于:

  • Callable 接口的 call() 方法是线程执行体,而 Runnable 接口的 run() 方法是线程执行体
  • Callable 接口的 call() 方法带返回值,声明抛出 Exception 异常

Callable 接口的源码如下:

package java.util.concurrent;

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

通过 FutureTask 类和 Callable 接口创建线程的步骤如下:

  • 实现 Callable 接口的 call() 方法
  • 创建 FutureTask 类的对象,传入 Callable 接口对象
  • 创建 Thread 类的对象,传入 FutureTask 对象

示例代码如下,采用了最为简洁的 Lambda 表达式来实现 Callable 接口对象:

import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class ThreadTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<Integer> task = new FutureTask<>(() -> 666);
        Thread thread = new Thread(task);
        thread.start();
        Integer result = task.get();
        System.out.println(result);
        // 666
    }
}

创建线程的本质

创建线程的本质在于 Thread 类的 run() 方法,源码如下:

@Override
public void run() {
    if (target != null) {
        target.run();
    }
}

在启动线程时,要调用线程对象的 start() 方法,而 start() 方法会调用原生的 start0() 方法,该方法会开启新线程,帮我们调用 Thread 对象的 run() 方法。根据上面的源码,执行流程分为两种情况:

  • 如果是通过继承 Thread 类创建的线程,会调用子类重写的 run() 方法
  • 如果为 Thread 类的构造函数传入了 Runnable 接口对象,则会调用接口对象 targetrun() 方法。

几种创建线程方式的比较

  • 通过继承 Thread 类创建线程的优点在于,可以直接在 run() 方法中通过 this 获取当前线程对象,不像其他方式要调用 Thread.currentThread() 才能获取。这种方式的缺点在于,Java 不支持类的多继承, Thread 类的子类无法继承其他类。
  • 通过接口实现类创建线程的优点在于,实现了线程执行体与 Thread 类的解耦,职责分明。
  • 一般推荐使用后两种创建线程的方案。