Java面试系列文章

面试必知必会(1):线程状态和创建方式


一、Java线程的状态:从生到死的完整生命周期

  Java线程的状态由Thread.State枚举类定义(JDK 5+),共6种状态:新建(NEW)运行(RUNNABLE)阻塞(BLOCKED)等待(WAITING)超时等待(TIMED_WAITING)终止(TERMINATED)。这些状态反映了线程在操作系统层面的调度状态与Java虚拟机(JVM)的管理逻辑。

1、状态定义与核心特征

1.1、NEW(新建状态)

  • 定义:当通过new Thread()创建线程对象后,尚未调用start()方法时,线程处于新建状态
  • 核心特征:
    • 线程对象已创建,此时线程仅存在于 Java 虚拟机的内存中,没有执行任何代码
    • 仅能调用start()方法进入 RUNNABLE 状态,调用其他方法(如run())不会改变状态,仅相当于普通方法调用

1.2、RUNNABLE(可运行状态)

  • 定义:调用start()方法后,线程进入RUNNABLE 状态,这是最核心的运行相关状态
  • 核心特征:
    • 该状态包含两个子状态:就绪(Ready)运行中(Running)
      • 就绪:线程已具备运行条件,等待 CPU 调度(由操作系统的线程调度器决定)
      • 运行中:线程获得 CPU 时间片,正在执行run()方法中的代码,时间片用完或被抢占后回到就绪态
    • 就绪状态不消耗 CPU 计算资源(不执行代码、不占用 CPU 时间片),仅占用 “调度资源”(在就绪队列排队)

1.3、BLOCKED(阻塞状态)

  • 定义:线程因等待获取对象监视器锁(synchronized 锁) 而阻塞时的状态
  • 核心特征:
    • 仅与 synchronized 锁相关,Lock 锁(如 ReentrantLock)的等待不会进入 BLOCKED 状态
    • 当线程获取到锁后,会从 BLOCKED 切换回 RUNNABLE 状态

1.4、WAITING(无超时等待状态)

  • 定义:线程进入无时间限制的等待状态,必须由其他线程显式唤醒,否则会永久等待
  • 触发场景(核心方法):
    • Object.wait():无参数,需配合synchronized使用,唤醒需Object.notify()/notifyAll()
    • Thread.join():无参数,等待目标线程执行完毕
    • LockSupport.park():无参数,唤醒需LockSupport.unpark(线程对象)
  • 核心特征:
    • BLOCKED/WAITING/TIMED_WAITING不占用 CPU 资源,线程调度器会将其移出调度队列

1.5、TIMED_WAITING(超时等待状态)

  • 定义:线程进入有时间限制的等待状态,超时后会自动唤醒,也可被其他线程提前唤醒
  • 触发场景(核心方法):
    • Thread.sleep(long millis):最常用,让线程休眠指定时间
    • Object.wait(long timeout):带超时参数的等待,超时自动唤醒
    • Thread.join(long millis):带超时参数的等待
    • LockSupport.parkNanos(long nanos)/parkUntil(long deadline):带超时的 park 方法
  • 核心特征:
    • 与 WAITING 的唯一区别是有超时时间,避免永久等待
    • wait/join和synchronized绑定,会释放锁sleep/park 不绑定,不会释放锁

1.6、TERMINATED(终止状态)

  • 定义:线程执行完毕(run()方法正常结束)或因异常终止后,进入终止状态
  • 核心特征:
    • 终止状态的线程无法再次启动(调用start()会抛出IllegalThreadStateException)
    • 线程的资源会被 JVM 回收,但线程对象本身仍存在(可通过对象引用获取状态)

2、状态转换的典型流程

2.1、NEW → RUNNABLE

  调用start()方法后,线程进入就绪队列(RUNNABLE)。注意:直接调用run()方法不会启动新线程,仅会作为普通方法在当前线程执行!

Thread t = new Thread(() -> System.out.println("Running"));
t.run(); // 错误:在主线程执行,不启动新线程
t.start(); // 正确:进入RUNNABLE状态,由JVM调度执行

2.2、RUNNABLE → BLOCKED

  当线程尝试进入synchronized修饰的代码块/方法时,若锁已被其他线程持有,当前线程进入BLOCKED状态,直到锁被释放并竞争成功。

public class BlockedStateDemo {
    private static final Object lock = new Object();
    
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (lock) {
                try { Thread.sleep(1000); } catch (InterruptedException e) {}
            }
        });
        
        Thread t2 = new Thread(() -> {
            synchronized (lock) { // t1持有锁,t2进入BLOCKED
                System.out.println("t2 acquired lock");
            }
        });
        
        t1.start();
        try { Thread.sleep(100); } catch (InterruptedException e) {} // 确保t1先获取锁
        t2.start();
    }
}

通过jstack <PID>命令可观察到t2的状态为BLOCKED (on object monitor)

2.3、RUNNABLE → WAITING/TIMED_WAITING

  • WAITING:调用wait()(需先获取对象锁)、join()(等待目标线程终止)、park()无超时参数的方法
  • TIMED_WAITING:调用sleep(time)wait(time)join(time)parkNanos(time)等带超时参数的方法
public class WaitingStateDemo {
    private static final Object lock = new Object();
    
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            synchronized (lock) {
                try {
                    lock.wait(); // 释放锁,进入WAITING(需其他线程notify)
                } catch (InterruptedException e) {}
            }
        });
        
        Thread t2 = new Thread(() -> {
            try {
                Thread.sleep(2000); // TIMED_WAITING状态,2秒后自动唤醒
            } catch (InterruptedException e) {}
        });
        
        t1.start();
        t2.start();
        Thread.sleep(100); // 确保t1已进入wait()
        System.out.println("t1 state: " + t1.getState()); // 输出WAITING
        System.out.println("t2 state: " + t2.getState()); // 输出TIMED_WAITING
    }
}

2.4、任意状态 → TERMINATED

  线程执行完run()方法或因未捕获异常退出,进入TERMINATED。

Thread t = new Thread(() -> {});
t.start();
t.join(); // 等待t终止
System.out.println(t.getState()); // TERMINATED
t.start(); // 抛出异常:IllegalThreadStateException

3、常见误区澄清

  • RUNNABLE vs 操作系统线程状态:Java的RUNNABLE包含操作系统的"就绪"(Ready)"运行"(Running),即线程可能在等待CPU时间片(就绪)或正在执行(运行),但JVM统一标记为RUNNABLE
  • BLOCKED vs WAITING:BLOCKED是因竞争锁失败被动阻塞;WAITING是主动调用方法释放锁并等待唤醒(如wait()会释放锁)
  • sleep()不会释放锁:调用sleep(time)时,线程仍持有synchronized锁,其他线程无法进入该锁的代码块

二、Java线程的创建方式:从基础到进阶

  Java提供多种线程创建方式,核心差异在于对线程控制权的分配(继承vs组合)、是否支持返回值、异常处理能力等。以下从传统方式到现代并发工具逐一解析。

1、继承Thread类(基础但不推荐)

  • 实现方式:重写Thread类的run()方法,通过start()启动线程
/**
 * 继承Thread类创建线程
 */
public class ThreadExtendDemo extends Thread {
    // 重写run方法,定义线程执行逻辑
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            // Thread.currentThread().getName()获取当前线程名称
            System.out.println("继承Thread的线程:" + Thread.currentThread().getName() + ",执行次数:" + i);
            try {
                Thread.sleep(100); // 模拟耗时操作
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        // 创建线程对象
        ThreadExtendDemo thread1 = new ThreadExtendDemo();
        ThreadExtendDemo thread2 = new ThreadExtendDemo();
        
        // 设置线程名称(便于调试)
        thread1.setName("线程1");
        thread2.setName("线程2");
        
        // 启动线程(核心:调用start(),而非直接调用run())
        thread1.start();
        thread2.start();
        
        // 错误示例:直接调用run(),仅为普通方法调用,不会创建新线程
        // thread1.run();
    }
}
  • 优点:简单直观,适合快速验证小功能
  • 缺点
    • 单继承限制:Java不支持多继承,若类已继承其他父类则无法使用
    • 线程与任务强耦合Thread既是线程载体又是任务逻辑载体,不符合"组合优于继承"原则
    • 无法获取返回值:需通过共享变量或回调间接传递结果,增加复杂度

2、实现Runnable接口(推荐的基础方式)

  • 实现方式:实现Runnable接口的run()方法,作为参数传入Thread构造器
/**
 * 实现Runnable接口创建线程
 */
public class RunnableImplDemo implements Runnable {
    // 共享变量(多线程共享)
    private int count = 0;

    // 实现run方法,定义线程执行逻辑
    @Override
    public void run() {
        synchronized (this) { // 同步代码块,保证线程安全
            for (int i = 0; i < 5; i++) {
                count++;
                System.out.println("实现Runnable的线程:" + Thread.currentThread().getName() + 
                                   ",count值:" + count);
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) {
        // 创建Runnable实现类对象(线程逻辑载体)
        RunnableImplDemo runnable = new RunnableImplDemo();
        
        // 创建Thread对象,传入Runnable实例
        Thread thread1 = new Thread(runnable, "线程A");
        Thread thread2 = new Thread(runnable, "线程B");
        
        // 启动线程
        thread1.start();
        thread2.start();
    }
}
  • 优点
    • 规避单继承限制:实现接口后仍可继承其他类
    • 解耦线程逻辑与对象:同一 Runnable 实例可被多个 Thread 复用
    • 便于共享资源:多个线程可共享同一个 Runnable 对象的成员变量
  • 缺点:仍无法获取线程执行结果(无返回值)

3、实现Callable接口+FutureTask(支持返回值与异常)

  • 实现方式:Callable<V>是泛型接口,其call()方法可返回结果并抛出异常;需通过FutureTask(实现了RunnableFuture)包装后传入Thread
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * 实现Callable接口创建线程(带返回值)
 */
public class CallableImplDemo implements Callable<Integer> {
    // 线程执行的任务:计算1~n的和
    private int n;

    public CallableImplDemo(int n) {
        this.n = n;
    }

    // 实现call方法(有返回值,可抛出异常)
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= n; i++) {
            sum += i;
            System.out.println("Callable线程:" + Thread.currentThread().getName() + 
                               ",计算到:" + i + ",当前和:" + sum);
            Thread.sleep(100);
        }
        return sum; // 返回执行结果
    }

    public static void main(String[] args) {
        // 1. 创建Callable实现类对象
        Callable<Integer> callable = new CallableImplDemo(10);
        
        // 2. 创建FutureTask对象(封装Callable,用于获取结果)
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        
        // 3. 创建Thread对象,传入FutureTask(FutureTask实现了Runnable接口)
        Thread thread = new Thread(futureTask, "计算线程");
        
        // 4. 启动线程
        thread.start();

        // 5. 获取线程执行结果(get()方法会阻塞(等待状态),直到线程执行完毕)
        try {
            Integer result = futureTask.get();
            System.out.println("线程执行结果:1~10的和 = " + result);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}
  • 优点
    • 支持返回值:通过FutureTask.get()获取call()的执行结果
    • 异常处理:可捕获call()抛出的受检异常(如Exception),避免线程静默崩溃
    • 超时控制:get(timeout)防止无限阻塞
  • 缺点:需额外包装(FutureTask),代码稍复杂
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐