Java并发编程理解02:线程的启动(Thread, Runnable, Callable, Future and FutureTask)

目录

Java 程序天生就是多线程的

线程的启动

启动

1. 继承Thread类

2. 实现 Runnable 接口

Thread 和 Runnable 的区别

Callable 、Future 和 FutureTask

Callable

Future

FutureTask

面试题


认识java里的线程

Java 程序天生就是多线程的

Java程序本质上就是多线程的,即使表面上看起来只有一个主线程在运行。这是因为JVM在启动时会自动创建多个关键的后台线程来协助运行和管理程序。我们可以通过线程转储或监控工具观察到这些系统线程的存在:

  1. main线程(用户线程)

    这是程序的入口点,负责执行main()方法和后续的用户代码逻辑。虽然用户通常只感知到这个线程,但它仅是JVM线程生态中的一个组成部分。
  2. JVM管理的核心系统线程(守护线程)

    • Monitor Ctrl-Break:用于监控中断信号(如IDE调试时的Ctrl+Break快捷键),部分环境可能不显示此线程。

    • Attach Listener:处理JVM工具接口(JVMTI)的请求,支持动态加载代理、内存快照(heap dump)、线程转储(thread dump)等诊断操作。

    • Signal Dispatcher:将操作系统信号(如SIGTERM终止信号)转发给JVM内部处理器。

    • Finalizer:调用对象的finalize()方法进行资源清理,属于垃圾回收的终结阶段。

    • Reference Handler:管理特殊引用(软引用、弱引用、虚引用)的入队和清理工作。

这些系统线程的生命周期由JVM控制,多数以守护线程(Daemon Thread)形式存在,不会阻止JVM退出。或许会因为DK版本或运行环境(如不同IDE)导致线程列表略有差异,但多线程架构的本质不变。即使用户未显式创建线程,JVM的后台线程依然在并行执行任务(如垃圾回收、信号处理),这充分体现了Java的多线程基因。

我们可以通过具体的代码对Java程序运行的线程作相应的查看:

public class ThreadOnlyMain {
    public static void main(String[] args) {
        //Java 虚拟机线程系统的管理接口
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        // 仅仅获取线程和线程堆栈信息,不需要获取同步的monitor和synchronizer信息
        ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
        // 遍历并打印线程ID和线程名称信息
        for (ThreadInfo threadInfo : threadInfos) {
            System.out.println("[" + threadInfo.getThreadId() + "] "
                    + threadInfo.getThreadName());
        }
    }
}

以下是相应的运行结果:

线程的启动

我们可以看到上面都是我们启动主程序时JVM自启动的系统线程,那我们要怎么操控自己的线程呢?下面我们一起学习并发编程:

启动
1. 继承Thread类
  1. 定义一个类继承Thread

  2. 重写 run() 方法;

  3. 创建实例并调用 start() 启动线程。

import java.util.concurrent.TimeUnit;

public class OwnThread {
    //定义一个类继承 Thread
    private static class UseThread extends Thread{
        //重写 run() 方法
        @Override
        public void run() {
            super.run();
            try {
                TimeUnit.MILLISECONDS.sleep(1);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("Thread started by extending Thread");
        }
    }

    public static void main(String[] args) {
        //创建实例并调用 start() 启动线程
        new UseThread().start();
    }
}

特点

  • 直接操作线程对象,简单但扩展性差(Java 不支持多重继承)。

  • 适用于简单场景。

2. 实现 Runnable 接口
  1. 定义一个类实现 Runnable 接口;
  2. 实现 run() 方法;
  3. 将 Runnable 实例传递给 Thread 构造函数;
  4. 调用 start() 启动线程。
public class OwnThread {
    // 定义一个类实现 Runnable 接口;
    private static class UseRunnable implements Runnable{
        // 实现 run() 方法;
        @Override
        public void run() {
            System.out.println("Thread started by implementing Runnable");
        }
    }

    public static void main(String[] args) {
        // 将 Runnable 实例传递给 Thread 构造函数;
        Thread thread = new Thread(new UseRunnable());
        // 启动线程
        thread. Start();
    }
}

Thread 和 Runnable 的区别

在 Java 并发编程中,Thread 和 Runnable 是两个核心概念,它们的职责和使用场景有本质区别。

Thread 是 Java中表示线程的唯一类,直接对应操作系统级别的线程(或轻量级线程,如协程在某些 JVM 的实现中)。负责管理线程的生命周期(如启动 start()、中断 interrupt()、等待结束 join())和线程属性(如名称、优先级等)。

 Runnable 只是对任务 (业务逻辑) 的抽象(接口)。 Runnable 仅定义了一个 run() 方法,用于封装需要在线程中执行的业务逻辑。而Thread可以接受任意一个 Runnable 的实例并执行。Runnable 是一个接口, 在它里面只声明了一个 run()方法,由于 run()方法返回值为 void 类型, 所以在执行完任务之后无法返回任何结果

Callable 、Future 和 FutureTask

从上面的描述中我们发现Runnable的方式有一定的局限性:

  • 无法返回结果run() 方法返回 void,任务执行后无法直接获取计算结果。
  • 无法抛出受检异常:run() 方法签名没有声明 throws,任务中抛出的受检异常必须自行捕获处理。

  • 缺乏任务状态追踪:无法判断任务是否完成、是否被取消,也无法主动取消任务。

class SumTask implements Runnable {
    private int a, b;
    private int result;

    public SumTask(int a, int b) {
        this.a = a;
        this.b = b;
    }

    @Override
    public void run() {
        result = a + b;
    }

    // 需要手动暴露结果(存在线程安全问题!)
    public int getResult() {
        return result;
    }
}

// 使用方式
SumTask task = new SumTask(3, 5);
Thread thread = new Thread(task);
thread.start();
thread.join(); // 必须等待线程结束
System.out.println(task.getResult()); // 输出 8

可以看到请问题有需通过共享变量传递结果,存在线程安全问题,且必须手动等待线程完成。在业务中假如需要通过多线程程序中返回值、追踪状态、处理异常等情况时,单单Runnable的方式就不够用了,于是就有了callable的引入。

Callable

Callable 位于 java.util.concurrent 包下, 它也是一个接口, 在它里面也只声明 了一个方法,只不 过这个方法叫做 call() ,这是一个泛型接口, call()函数返回的 类型就是传递进来的 V 类型。

Callable<V> 接口解决了 Runnable 的两个关键问题:

  • 支持返回值call() 方法返回泛型类型 V 的结果。

  • 允许抛出异常call() 方法声明了 throws Exception,可直接向上层传播异常。

Callable<Integer> task = () -> {
    int a = 3, b = 5;
    return a + b; // 直接返回结果
};
Future

为什么需要Future?虽然 Callable 可以返回结果,但任务的执行是异步的,需要一种机制能够查询任务状态是否完成、是否被取消;能够以阻塞或非阻塞方式获取结果;也能在任务未完成时主动终止

Future的主要方法有:

  • boolean cancel(boolean mayInterruptIfRunning):尝试取消任务(不一定成功)
  • boolean isCancelled():判断任务是否被取消
  • boolean isDone():判断任务是否完成(正常结束、异常、取消均视为完成)
  • V get():阻塞获取结果,直到任务完成或超时
  • V get(long timeout, TimeUnit unit):阻塞获取结果,最多等待指定时间

示例用法:

ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(() -> 3 + 5); // 提交 Callable 任务

// 非阻塞检查任务状态
if (future.isDone()) {
    System.out.println("Result: " + future.get());
} else {
    // 执行其他逻辑...
}

executor.shutdown();
FutureTask

为什么需要FutureTask? 因为 Future 只是一个接口,无法直接用来创建对象使用的。而FutureTask 是 Future 接口的一个实现类,同时实现了 Runnable 接口,扮演任务载体结果容器两个角色。既可作为 Runnable 提交给线程或线程池、又可以持有 Callable 任务的执行结果或异常。

那么在使用上要new一个Future Task实例,有两种办法:

Callable<Integer> task = () -> 3 + 5;
FutureTask<Integer> futureTask = new FutureTask<>(task);

// 方式 1:通过线程启动
new Thread(futureTask).start(); // 通过 Thread 启动 Runnable
System.out.println(futureTask.get());

// 方式 2:通过线程池提交
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(futureTask); // 提交 Runnable 或 Callable
executor.shutdown();

/*
线程池的 submit(Runnable) 方法内部会将 Runnable 包装成一个 Callable 适配器
(Executors.callable(Runnable, result)),最终以 Callable 形式处理。
但由于 FutureTask 本身已经是 Callable 的封装,线程池会直接执行其 run() 方法。
*/

在 Java 中,FutureTask 的设计非常巧妙,它同时实现了 Runnable 和 Future 接口,因此可以通过 Runnable 的形式提交任务,但内部封装的是 Callable 的逻辑。上面两种提交方式本质上都是通过 Runnable 提交的,但底层最终处理的是 Callable 任务。

三者之间的关系架构图:

面试题

那说了这么多,就到了大家都喜欢的面试题环节(手动狗头)

面试题:新启线程有几种方式?

在 Java 中,按照Thread class的源码注释上解释,新启线程的核心方式有两种:继承 Thread 类实现 Runnable 接口。这两种方式本质都是通过创建 Thread 对象并调用其 start() 方法启动线程,但设计理念和使用场景有所不同。

继承 Thread 类需要重写 run() 方法定义线程任务逻辑,直接通过子类实例的 start() 启动线程。这种方式简单直观,但缺点明显:Java 不支持多继承,若类已继承其他类则无法使用此方式,且将任务与线程强耦合,违背面向对象设计原则。因此更推荐实现 Runnable 接口,将任务逻辑封装在 run() 方法中,通过 Thread 构造函数传入 Runnable 实例来启动线程。这种方式解耦了任务与线程管理,支持多接口实现,任务对象可被多个线程共享,也更契合线程池等高级用法。

虽然后续演进中引入了 Callable、线程池等更强大的工具,但底层仍依赖 Runnable 或 Thread 的机制。因此,这两种方式始终是线程启用的基础,其中实现 Runnable 接口因其灵活性和扩展性成为工业级开发的首选。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值