跳到主要内容

线程基础

问题

Java 中创建线程有几种方式?线程的生命周期和状态有哪些?start()run() 有什么区别?

答案

创建线程的方式

Java 中创建线程本质上只有一种方式——构造 Thread 对象,但提交任务的途径有多种:

1. 继承 Thread 类

ExtendsThread.java
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 接口(推荐)

ImplementsRunnable.java
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 可以返回结果抛出受检异常

CallableExample.java
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. 线程池(实际开发推荐)

ThreadPoolExample.java
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线程执行结束(正常结束或异常终止)
RUNNABLE 包含运行中和就绪

Java 线程的 RUNNABLE 状态对应 OS 层面的 Ready 和 Running。线程拿到 CPU 时间片就是 Running,没拿到就是 Ready,但 Java 层面统一叫 RUNNABLE。

start() vs run()

StartVsRun.java
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 创建系统线程普通方法调用

线程常用方法

ThreadMethods.java
// 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 线程中断是一种协作机制,不会强制停止线程:

InterruptExample.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(); // 通知线程中断
不要使用 stop()

Thread.stop() 已被废弃,它会强制终止线程并释放所有锁,可能导致数据不一致。应该使用中断机制标志位来优雅停止线程。

守护线程

DaemonThread.java
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、使用线程池。

推荐使用线程池,原因:

  1. 避免频繁创建/销毁线程的开销
  2. 控制并发线程数量,防止资源耗尽
  3. 提供任务队列、拒绝策略等管理能力
  4. 方便统一监控和管理

Q2: Runnable 和 Callable 的区别?

答案

对比RunnableCallable
方法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()
所属类ThreadObject
是否释放锁不释放释放
使用条件任何地方必须在 synchronized 块中
唤醒方式超时自动醒来notify()/notifyAll() 或超时
用途暂停当前线程线程间通信

Q5: 为什么 wait() 必须在 synchronized 块中调用?

答案

wait() 的语义是"释放锁并进入等待",如果没有持有锁就调用,会抛出 IllegalMonitorStateException

这是为了避免竞态条件:如果不要求在同步块中,可能出现"检查条件 → 调用 wait()"之间条件已被改变但 wait 仍然执行的问题(Lost Wake-Up Problem)。

Q6: 如何优雅地停止一个线程?

答案

两种推荐方式:

  1. 中断机制:调用 thread.interrupt(),线程内部检查 isInterrupted() 并响应
  2. 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 或需要可靠完成的任务

相关链接