基于本文回答
0
评论

Java线程创建的四种方式

知识点图片

本文讲解了Java创建线程的四种方式:继承Thread、实现Runnable、实现Callable和使用线程池,并对比优劣,最终强烈推荐使用线程池。

在Java中创建线程主要有四种方式。我会从最基础到最推荐的方式逐一为你详细讲解,并附上代码示例和优缺点对比。

几种创建线程的方式概览

  1. 继承 Thread:最直接的方式,但不推荐。
  2. 实现 Runnable 接口:推荐的方式,将任务(Runnable)与线程(Thread)解耦。
  3. 实现 Callable 接口(配合 FutureTaskRunnable 的增强版,可以有返回值,可以抛出异常。
  4. 使用线程池(ExecutorService强烈推荐的生产级实践方式,能有效管理和复用线程资源。

方式一:继承 Thread

这是最直观的方式,让一个类直接成为一个线程类。

步骤:

  1. 创建一个类,继承自 java.lang.Thread
  2. 重写 run() 方法,将线程需要执行的逻辑代码放在 run() 方法中。
  3. 创建该类的实例。
  4. 调用实例的 start() 方法来启动线程(注意: 是调用 start() 而不是 run()!调用 run() 只是一个普通的方法调用,不会创建新线程)。

代码示例:

java
// 1. 定义一个继承自 Thread 的类
class MyThread extends Thread {
    // 2. 重写 run() 方法
    @Override
    public void run() {
        System.out.println("通过继承 Thread 类创建的线程正在运行...");
        System.out.println("当前线程名称: " + Thread.currentThread().getName());
    }
}

public class Main {
    public static void main(String[] args) {
        // 3. 创建线程实例
        MyThread thread1 = new MyThread();
        
        // 4. 启动线程
        thread1.start();
        
        System.out.println("Main 线程结束。");
    }
}

优缺点:

  • 优点:实现简单,代码直观易懂。
  • 缺点
    • 单继承局限:Java 不支持多重继承,如果你的类已经继承了另一个类,就无法再继承 Thread 类了。
    • 紧耦合:任务(run 方法中的逻辑)与线程的创建(Thread 类)紧密地扣合在一起,不符合面向对象的设计原则。

方式二:实现 Runnable 接口

这是更常用且推荐的方式,它将“任务”和“执行任务的线程”分离开。

步骤:

  1. 创建一个类,实现 java.lang.Runnable 接口。
  2. 实现接口中的 run() 方法,这里是线程的执行体。
  3. 创建该 Runnable 实现类的实例。
  4. 创建一个 Thread 对象,并将 Runnable 实例作为构造函数的参数传入。
  5. 调用 Thread 对象的 start() 方法。

代码示例:

java
// 1. 定义一个实现 Runnable 接口的类
class MyRunnable implements Runnable {
    // 2. 实现 run() 方法
    @Override
    public void run() {
        System.out.println("通过实现 Runnable 接口创建的线程正在运行...");
        System.out.println("当前线程名称: " + Thread.currentThread().getName());
    }
}

public class Main {
    public static void main(String[] args) {
        // 3. 创建 Runnable 实例 (这是任务)
        MyRunnable myRunnable = new MyRunnable();
        
        // 4. 创建 Thread 实例,并将任务传递给它
        Thread thread2 = new Thread(myRunnable);
        
        // 5. 启动线程
        thread2.start();

        System.out.println("Main 线程结束。");
    }
}

优缺点:

  • 优点
    • 解耦:将任务逻辑(Runnable)和线程机制(Thread)分离,结构清晰。
    • 无继承限制:你的任务类可以继承其他任何类,因为它只是实现一个接口。
    • 资源共享:多个线程可以共享同一个 Runnable 实例,方便实现资源共享。
  • 缺点:相比继承 Thread 类,代码稍微多一点点。

方式三:实现 Callable 接口

如果你的线程执行完任务后需要一个返回值,或者任务执行中可能抛出异常,Callable 是最好的选择。

步骤:

  1. 创建一个类,实现 java.util.concurrent.Callable<V> 接口,其中 V 是返回值的类型。
  2. 实现 call() 方法(注意不是 run()),这个方法可以有返回值,也可以 throws Exception
  3. 创建 Callable 实现类的实例。
  4. 使用 FutureTask<V> 来包装 Callable 对象。FutureTask 同时实现了 RunnableFuture 接口。
  5. FutureTask 对象作为参数创建 Thread 对象并启动。
  6. 通过 FutureTask 对象的 get() 方法获取线程执行的返回值(get() 方法会阻塞,直到任务完成)。

代码示例:

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

// 1. 定义一个实现 Callable 接口的类
class MyCallable implements Callable<String> {
    // 2. 实现 call() 方法,带返回值
    @Override
    public String call() throws Exception {
        System.out.println("通过实现 Callable 接口创建的线程正在运行...");
        Thread.sleep(2000); // 模拟耗时操作
        return "任务完成,这是返回值!";
    }
}

public class Main {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 3. 创建 Callable 实例
        MyCallable myCallable = new MyCallable();
        
        // 4. 使用 FutureTask 包装 Callable
        FutureTask<String> futureTask = new FutureTask<>(myCallable);
        
        // 5. 创建并启动线程
        Thread thread3 = new Thread(futureTask);
        thread3.start();

        System.out.println("Main 线程继续执行其他任务...");
        
        // 6. 获取线程返回值 (此方法会阻塞)
        String result = futureTask.get();
        System.out.println("获取到的返回值: " + result);

        System.out.println("Main 线程结束。");
    }
}

优缺点:

  • 优点
    • 可以有返回值:能够获取异步任务的执行结果。
    • 可以抛出异常:可以在 call 方法中抛出受检异常,并在主线程中捕获。
  • 缺点:实现稍微复杂一些。

方式四:使用线程池 (ExecutorService)

在实际企业级开发中,这是最推荐的方式。手动创建和销毁线程开销很大,线程池可以复用已创建的线程,统一管理,避免资源耗尽。

步骤:

  1. 使用 Executors 工厂类创建 ExecutorService (线程池)。常见的有 newFixedThreadPool, newCachedThreadPool, newSingleThreadExecutor
  2. 创建你的任务(RunnableCallable 的实例)。
  3. 调用线程池的 submit()execute() 方法提交任务。
    • execute(Runnable): 提交不需要返回值的任务。
    • submit(Runnable/Callable): 提交任务,返回一个 Future 对象,可以用来获取结果或检查状态。
  4. (重要)当不再需要线程池时,调用 shutdown() 方法平滑地关闭它。

代码示例:

java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class Main {
    public static void main(String[] args) throws Exception {
        // 1. 创建一个固定大小的线程池
        ExecutorService executorService = Executors.newFixedThreadPool(3);

        // 提交 Runnable 任务
        executorService.execute(() -> {
            System.out.println("线程池中的线程(Runnable)正在执行... " + Thread.currentThread().getName());
        });

        // 提交 Callable 任务
        Future<Integer> future = executorService.submit(() -> {
            System.out.println("线程池中的线程(Callable)正在执行... " + Thread.currentThread().getName());
            return 100;
        });

        // 获取 Callable 任务的结果
        Integer result = future.get();
        System.out.println("Callable 任务的返回结果: " + result);

        // 4. 关闭线程池
        executorService.shutdown(); // 不再接收新任务,但会执行完已提交的任务
    }
}

上面的例子也使用了 Lambda 表达式,这是 Java 8 以来的语法糖,可以极大地简化 RunnableCallable 的匿名内部类写法。

优缺点:

  • 优点
    • 资源管理:统一管理线程生命周期,降低资源消耗。
    • 性能提升:通过复用线程,避免了频繁创建和销毁线程的开销。
    • 控制并发:可以控制同时并发的线程数量,防止系统过载。
    • 功能强大:提供了定时执行、定期执行、获取结果等丰富功能。
  • 缺点:理解和使用比前几种方式稍微复杂。

总结与最佳实践

创建方式 核心 优点 缺点 推荐度
继承 Thread extends Thread 简单直观 单继承限制、耦合度高 ★☆☆☆☆ (不推荐)
实现 Runnable implements Runnable 解耦、无继承限制、可资源共享 代码稍多 ★★★★☆ (推荐)
实现 Callable implements Callable 可返回结果、可抛出异常 实现稍复杂 ★★★★☆ (特定场景推荐)
使用线程池 ExecutorService 资源复用、性能高、易于管理、功能强大 学习成本稍高 ★★★★★ (强烈推荐)

最佳实践建议:

  1. 首选线程池:在任何需要处理多个异步任务的生产环境中,都应该优先考虑使用线程池。
  2. 任务选择
    • 如果任务不需要返回值,使用 Runnable
    • 如果任务需要返回值或可能抛出异常,使用 Callable
  3. 拥抱 Lambda:在 Java 8及以上版本中,使用 Lambda 表达式来创建 RunnableCallable 实例,会让代码更简洁、更具可读性。
  4. 理解基础:尽管不推荐直接使用 extends Thread,但理解它是学习Java多线程的基础。
右滑查看面试常问