Java 多线程是面试高频考点,也是日常开发绕不开的话题。这篇文章从线程的创建方式讲起,深入到线程状态的流转,配合代码示例把基础打牢。
线程的创建方式
Java 中创建线程主要有两种经典方式,加上 JDK5 引入的 Callable,一共三种常见做法。
方式一:继承 Thread 类
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " - " + i);
}
}
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.start(); // 启动线程,调用 run()
t2.start();
}
}
注意要调用 start() 而不是直接调用 run()。直接调用 run() 只是在当前线程中执行方法体,并不会启动新线程。
方式二:实现 Runnable 接口
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " - " + i);
}
}
public static void main(String[] args) {
Thread t1 = new Thread(new MyRunnable(), "线程A");
Thread t2 = new Thread(new MyRunnable(), "线程B");
t1.start();
t2.start();
}
}
实现 Runnable 接口是更推荐的方式,原因有两个:Java 不支持多继承,继承了 Thread 就不能再继承其他类;Runnable 将任务逻辑和线程对象解耦,同一个 Runnable 实例可以被多个线程共享。
方式三:实现 Callable + FutureTask
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i;
}
return sum;
}
public static void main(String[] args) throws Exception {
FutureTask<Integer> task = new FutureTask<>(new MyCallable());
Thread t = new Thread(task);
t.start();
// get() 会阻塞当前线程,直到 call() 执行完毕
Integer result = task.get();
System.out.println("计算结果: " + result); // 5050
}
}
Callable 和 Runnable 的核心区别:Callable 的 call() 方法有返回值,且可以抛出受检异常。
线程的生命周期
Java 线程在整个生命周期中会经历以下六种状态(定义在 Thread.State 枚举中):
NEW(新建) -> 线程对象被创建,但尚未调用 start()。
RUNNABLE(可运行) -> 调用 start() 后进入此状态。注意 RUNNABLE 包含了操作系统层面的 Ready 和 Running 两种状态——线程可能在等待 CPU 时间片,也可能正在执行。
BLOCKED(阻塞) -> 线程试图获取一个被其他线程持有的 synchronized 锁时,进入 BLOCKED 状态,直到获取到锁。
WAITING(等待) -> 线程调用了 Object.wait()、Thread.join()(不带超时)或 LockSupport.park() 后进入此状态,需要等待其他线程显式唤醒。
TIMED_WAITING(超时等待) -> 与 WAITING 类似,但有超时时间。通过 Thread.sleep(ms)、Object.wait(ms)、Thread.join(ms) 等方法进入。
TERMINATED(终止) -> run() 方法执行完毕或抛出未捕获异常,线程结束。
状态流转示意:
NEW --start()--> RUNNABLE --获取锁失败--> BLOCKED --获取到锁--> RUNNABLE
| |
+--wait()/join()---> WAITING ---notify()------>+
| |
+--sleep(ms)/wait(ms)--> TIMED_WAITING --超时/唤醒-->+
|
+--run()结束--> TERMINATED
常用的线程控制方法
sleep — 让当前线程休眠
public class SleepDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println("开始: " + System.currentTimeMillis());
Thread.sleep(2000); // 休眠 2 秒
System.out.println("结束: " + System.currentTimeMillis());
}
}
sleep() 不会释放锁。如果当前线程持有某个对象的 synchronized 锁,sleep 期间其他线程依然无法进入同步块。
join — 等待另一个线程执行完毕
public class JoinDemo {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
try {
Thread.sleep(2000);
System.out.println("子线程执行完毕");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t.start();
System.out.println("等待子线程...");
t.join(); // 主线程阻塞,直到 t 执行完毕
System.out.println("主线程继续执行");
}
}
输出顺序是确定的:等待子线程 -> 子线程执行完毕 -> 主线程继续执行。
yield — 让出 CPU 时间片
Thread.yield() 提示调度器当前线程愿意让出 CPU,但调度器可以忽略这个提示。实际开发中很少使用。
synchronized 基础
synchronized 是 Java 中最基本的同步机制,用于保证同一时刻只有一个线程执行某段代码。
public class Counter {
private int count = 0;
// 同步方法:锁的是 this 对象
public synchronized void increment() {
count++;
}
// 同步代码块:可以指定锁对象
public void decrement() {
synchronized (this) {
count--;
}
}
public int getCount() {
return count;
}
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
counter.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
counter.increment();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("最终计数: " + counter.getCount()); // 20000
}
}
如果去掉 synchronized,最终结果几乎不可能是 20000,因为 count++ 不是原子操作——它实际上包含读取、加一、写回三个步骤,多线程并发时会出现竞态条件。
小结
线程基础看似简单,但很多并发 Bug 都源于对基础概念理解不透。关键记住几点:start() 和 run() 的区别、六种线程状态的流转条件、sleep 不释放锁而 wait 会释放锁、synchronized 的锁对象是谁。后续文章会继续深入 JUC 包中的并发工具。