目录
认识java里的线程
Java 程序天生就是多线程的
Java程序本质上就是多线程的,即使表面上看起来只有一个主线程在运行。这是因为JVM在启动时会自动创建多个关键的后台线程来协助运行和管理程序。我们可以通过线程转储或监控工具观察到这些系统线程的存在:
-
main线程(用户线程)
这是程序的入口点,负责执行main()方法和后续的用户代码逻辑。虽然用户通常只感知到这个线程,但它仅是JVM线程生态中的一个组成部分。 -
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类
-
定义一个类继承Thread
-
重写
run()方法; -
创建实例并调用
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 接口
- 定义一个类实现
Runnable接口; - 实现
run()方法; - 将
Runnable实例传递给Thread构造函数; - 调用
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 接口因其灵活性和扩展性成为工业级开发的首选。


1099

被折叠的 条评论
为什么被折叠?



