线程基础
问题
Java 中创建线程有几种方式?线程的生命周期和状态有哪些?start() 和 run() 有什么区别?
答案
创建线程的方式
Java 中创建线程本质上只有一种方式——构造 Thread 对象,但提交任务的途径有多种:
1. 继承 Thread 类
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("线程运行: " + Thread.currentThread().getName());
}
}
// 使用
MyThread t = new MyThread();
t.start(); // 启动新线程
继承 Thread 后就无法再继承其他类,Java 不支持多继承,因此实际开发中不推荐这种方式。
2. 实现 Runnable 接口(推荐)
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("线程运行: " + Thread.currentThread().getName());
}
}
// 使用
Thread t = new Thread(new MyRunnable());
t.start();
// Lambda 简化
Thread t2 = new Thread(() -> System.out.println("Lambda 线程"));
t2.start();
3. 实现 Callable 接口 + FutureTask
与 Runnable 不同,Callable 可以返回结果和抛出受检异常:
import java.util.concurrent.*;
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
Thread.sleep(1000);
return "任务完成";
}
}
// 使用 FutureTask 包装
FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
Thread t = new Thread(futureTask);
t.start();
// 获取结果(阻塞等待)
String result = futureTask.get();
System.out.println(result); // "任务完成"
4. 线程池(实际开发推荐)
ExecutorService executor = Executors.newFixedThreadPool(4);
// 提交 Runnable
executor.submit(() -> System.out.println("线程池任务"));
// 提交 Callable,获取 Future
Future<String> future = executor.submit(() -> "返回值");
String result = future.get();
executor.shutdown();
| 方式 | 返回值 | 异常 | 灵活性 |
|---|---|---|---|
| 继承 Thread | 无 | 不能抛出受检异常 | 低(单继承限制) |
| 实现 Runnable | 无 | 不能抛出受检异常 | 高 |
| 实现 Callable | 有 | 可以抛出受检异常 | 高 |
| 线程池 | 可选 | 可选 | 最高(推荐) |
线程生命周期(6 种状态)
Java 线程状态定义在 Thread.State 枚举中:
| 状态 | 说明 |
|---|---|
| NEW | 线程对象已创建,但尚未调用 start() |
| RUNNABLE | 调用 start() 后进入,包含就绪和运行中两个子状态 |
| BLOCKED | 等待获取 synchronized 锁 |
| WAITING | 无限期等待,调用 wait()、join()、LockSupport.park() |
| TIMED_WAITING | 有限期等待,调用 sleep(n)、wait(n)、join(n) |
| TERMINATED | 线程执行结束(正常结束或异常终止) |
Java 线程的 RUNNABLE 状态对应 OS 层面的 Ready 和 Running。线程拿到 CPU 时间片就是 Running,没拿到就是 Ready,但 Java 层面统一叫 RUNNABLE。
start() vs run()
Thread t = new Thread(() -> {
System.out.println("当前线程: " + Thread.currentThread().getName());
});
// 调用 start():启动新线程,在新线程中执行 run()
t.start(); // 输出: 当前线程: Thread-0
// 直接调用 run():在当前线程中执行,不会创建新线程
t.run(); // 输出: 当前线程: main
| 对比 | start() | run() |
|---|---|---|
| 是否创建新线程 | 是 | 否 |
| 执行线程 | 新线程 | 当前线程 |
| 能否多次调用 | 不能(抛 IllegalThreadStateException) | 可以 |
| 底层操作 | 调用 native start0() → JVM 创建系统线程 | 普通方法调用 |
线程常用方法
// sleep:当前线程休眠,不释放锁
Thread.sleep(1000);
// yield:让出 CPU 时间片,回到就绪状态(不保证一定让出)
Thread.yield();
// join:等待目标线程执行完毕
Thread t = new Thread(() -> heavyTask());
t.start();
t.join(); // 主线程阻塞,直到 t 执行完
// interrupt:中断线程(设置中断标志位)
t.interrupt();
// 检查中断状态
Thread.currentThread().isInterrupted(); // 不清除标志位
Thread.interrupted(); // 清除标志位
线程中断机制
Java 线程中断是一种协作机制,不会强制停止线程:
Thread t = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
System.out.println("工作中...");
Thread.sleep(1000);
} catch (InterruptedException e) {
// sleep/wait/join 被中断时抛出此异常
// 异常抛出后中断标志位会被清除,需要重新设置
System.out.println("收到中断信号,准备退出");
Thread.currentThread().interrupt(); // 重新设置中断标志
break;
}
}
System.out.println("线程退出");
});
t.start();
Thread.sleep(3000);
t.interrupt(); // 通知线程中断
Thread.stop() 已被废弃,它会强制终止线程并释放所有锁,可能导致数据不一致。应该使用中断机制或标志位来优雅停止线程。
守护线程
Thread daemon = new Thread(() -> {
while (true) {
System.out.println("守护线程运行中...");
try { Thread.sleep(1000); } catch (InterruptedException e) { break; }
}
});
// 必须在 start() 之前设置
daemon.setDaemon(true);
daemon.start();
// 当所有非守护线程结束时,守护线程自动终止
// GC 线程就是典型的守护线程
常见面试问题
Q1: 创建线程有几种方式?推荐哪种?
答案:
本质上只有一种——构造 Thread 对象。但提交任务有 4 种方式:继承 Thread、实现 Runnable、实现 Callable、使用线程池。
推荐使用线程池,原因:
- 避免频繁创建/销毁线程的开销
- 控制并发线程数量,防止资源耗尽
- 提供任务队列、拒绝策略等管理能力
- 方便统一监控和管理
Q2: Runnable 和 Callable 的区别?
答案:
| 对比 | Runnable | Callable |
|---|---|---|
| 方法 | void run() | V call() throws Exception |
| 返回值 | 无 | 有(泛型) |
| 异常 | 不能抛出受检异常 | 可以抛出受检异常 |
| 配合 | Thread、线程池 | FutureTask、线程池 |
如果不需要返回值用 Runnable,需要返回值或异常处理用 Callable。
Q3: 线程的 6 种状态分别是什么?
答案:
NEW(新建)→ RUNNABLE(可运行,包含就绪和运行中)→ BLOCKED(阻塞,等待 synchronized)→ WAITING(无限期等待)→ TIMED_WAITING(有限期等待)→ TERMINATED(终止)。
注意 BLOCKED 只在等待 synchronized 锁时出现,等待 ReentrantLock 时线程状态是 WAITING(因为底层用 LockSupport.park())。
Q4: sleep() 和 wait() 的区别?
答案:
| 对比 | sleep() | wait() |
|---|---|---|
| 所属类 | Thread | Object |
| 是否释放锁 | 不释放 | 释放 |
| 使用条件 | 任何地方 | 必须在 synchronized 块中 |
| 唤醒方式 | 超时自动醒来 | notify()/notifyAll() 或超时 |
| 用途 | 暂停当前线程 | 线程间通信 |
Q5: 为什么 wait() 必须在 synchronized 块中调用?
答案:
wait() 的语义是"释放锁并进入等待",如果没有持有锁就调用,会抛出 IllegalMonitorStateException。
这是为了避免竞态条件:如果不要求在同步块中,可能出现"检查条件 → 调用 wait()"之间条件已被改变但 wait 仍然执行的问题(Lost Wake-Up Problem)。
Q6: 如何优雅地停止一个线程?
答案:
两种推荐方式:
- 中断机制:调用
thread.interrupt(),线程内部检查isInterrupted()并响应 - volatile 标志位:共享一个
volatile boolean标志,线程循环检查
// 方式一:中断
while (!Thread.currentThread().isInterrupted()) {
// 工作逻辑
}
// 方式二:volatile 标志位
private volatile boolean stopped = false;
while (!stopped) {
// 工作逻辑
}
不要使用 Thread.stop()(强制终止,数据不一致)或 Thread.suspend()(容易死锁)。
Q7: join() 的原理是什么?
答案:
join() 底层调用的是 wait()。当调用 t.join() 时,当前线程进入 t 对象的等待队列。当 t 线程执行完毕后,JVM 会调用 t.notifyAll() 唤醒所有在 t 上等待的线程。
// join() 的核心源码
public final synchronized void join(long millis) throws InterruptedException {
while (isAlive()) {
wait(millis); // 当前线程在 this(Thread 对象)上 wait
}
}
Q8: 守护线程和用户线程的区别?
答案:
- 用户线程(User Thread):前台线程,JVM 会等待所有用户线程执行完毕才退出
- 守护线程(Daemon Thread):后台线程,当所有用户线程结束时,守护线程自动终止
典型守护线程:GC 线程、Finalizer 线程。
注意事项:
setDaemon(true)必须在start()之前调用- 守护线程中的 finally 块不一定执行(JVM 退出时强制终止)
- 守护线程不适合执行 I/O 或需要可靠完成的任务
相关链接
- Thread - Java 17 API
- Runnable - Java 17 API
- Callable - Java 17 API
- Java 线程池 - 线程池详细解析
- JVM 内存结构 - 线程私有区域(栈、程序计数器)