Java 线程核心概念与生命周期详解

一、进程与线程的核心区别

在操作系统和 Java 程序运行中,进程与线程是实现任务执行的基础单元,但二者在资源占用、调度效率等方面存在本质差异,具体对比如下:

对比维度 进程(Process) 线程(Thread)
资源占用 独立的内存空间(代码段、数据段、堆、栈),资源消耗高 共享所属进程的内存空间(堆、方法区),仅拥有独立栈和程序计数器,资源消耗低
调度单位 操作系统调度的基本单位(粒度粗) CPU 调度的基本单位(粒度细,调度效率更高)
通信复杂度 需通过进程间通信(IPC,如管道、Socket),复杂度高 可通过共享内存(如静态变量、共享对象)直接通信,复杂度低
独立性 进程间相互独立,一个进程崩溃不影响其他进程 线程依赖于进程,一个线程崩溃可能导致整个进程崩溃
创建与销毁成本 成本高(需分配独立内存、初始化资源) 成本低(复用进程资源,仅需初始化栈和程序计数器)

Java 中的体现:一个 Java 程序启动后,默认对应一个进程(如 java -jar xxx.jar 启动的进程),进程内至少包含一个主线程(main 线程),开发者可通过代码创建多个子线程,实现任务并行执行。

二、并行与串行的区别

并行与串行是任务执行的两种核心模式,直接影响程序的执行效率,尤其在多线程场景中至关重要:

对比维度 串行(Serial) 并行(Parallel)
执行方式 多个任务按顺序执行,一个任务完成后再执行下一个 多个任务在同一时间内同时执行(需多核 CPU 支持)
执行效率 效率低,总耗时 = 所有任务耗时之和 效率高,总耗时 ≈ 耗时最长的单个任务耗时(理想情况)
CPU 利用 仅占用单个 CPU 核心,多核 CPU 资源无法充分利用 可占用多个 CPU 核心,充分利用多核资源
适用场景 任务间存在强依赖(如“先读取文件,再解析文件”) 任务间无依赖或弱依赖(如“同时下载多个文件”)

Java 中的示例

  • 串行:main 线程中依次执行“读取数据库数据 → 处理数据 → 写入文件”,三个步骤按顺序完成。
  • 并行:创建 3 个线程,分别执行“读取数据库数据”“处理数据”“写入文件”(若任务间无依赖),或创建 10 个线程同时处理 10 批独立数据,利用多核 CPU 加速执行。

三、Java 实现线程的 4 种方式

Java 提供了多种创建线程的途径,不同方式适用于不同场景,核心区别在于“是否继承 Thread 类”和“是否实现任务逻辑与线程控制分离”:

1. 继承 Thread 类

通过继承 java.lang.Thread 类,重写 run() 方法定义任务逻辑,调用 start() 方法启动线程(start() 会触发 JVM 调用 run(),而非直接调用 run())。

public class MyThread extends Thread {
    @Override
    public void run() {
        // 线程执行的任务逻辑
        System.out.println("线程1执行:" + Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start(); // 启动线程,触发 run() 执行
    }
}

缺点:Java 单继承机制限制,继承 Thread 后无法再继承其他类。

2. 实现 Runnable 接口

实现 java.lang.Runnable 接口的 run() 方法,将任务逻辑与线程控制分离,通过 Thread 类包装后启动。

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("线程2执行:" + Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        // 将 Runnable 任务包装到 Thread 中
        Thread thread = new Thread(new MyRunnable());
        thread.start();
    }
}

优点:避免单继承限制,可同时实现其他接口;适合多线程共享同一任务实例(如共享计数器)。

3. 实现 Callable 接口(带返回值)

java.util.concurrent.Callable 接口与 Runnable 类似,但 call() 方法可返回结果且能抛出异常,需配合 FutureTask 获取结果(FutureTask 实现了 RunnableFuture 接口,可作为 Thread 的构造参数)。

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 {
        Callable<Integer> callable = new MyCallable();
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        new Thread(futureTask).start();

        // 获取任务结果(若任务未完成,get() 会阻塞)
        Integer result = futureTask.get();
        System.out.println("计算结果:" + result); // 输出 5050
    }
}

适用场景:需要获取线程执行结果的场景(如异步计算、多任务汇总结果)。

4. 使用线程池(推荐)

通过 java.util.concurrent.ExecutorService 线程池创建线程,无需手动管理线程的创建与销毁,降低资源消耗。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolDemo {
    public static void main(String[] args) {
        // 创建固定大小的线程池(3个线程)
        ExecutorService executorService = Executors.newFixedThreadPool(3);

        // 提交任务(Runnable 或 Callable)
        for (int i = 1; i <= 5; i++) {
            int taskId = i;
            executorService.submit(() -> {
                System.out.println("线程池任务" + taskId + "执行:" + Thread.currentThread().getName());
            });
        }

        // 关闭线程池(先拒绝新任务,再等待已提交任务完成)
        executorService.shutdown();
    }
}

优点:复用线程、控制并发数、避免线程频繁创建销毁的开销,是生产环境的首选方式。

四、Java 线程的生命周期

根据 JDK 官方定义,Java 线程的生命周期包含 6 个状态,状态转换通过 Thread.State 枚举类定义,具体如下:

1. 状态定义(Thread.State)

状态名称 核心含义
NEW(新建) 线程对象已创建,但未调用 start() 方法,未与操作系统底层线程关联
RUNNABLE(可运行) 调用 start() 后,线程处于“就绪”或“运行”状态:
- 就绪:等待 CPU 调度
- 运行:正在占用 CPU 执行任务
BLOCKED(阻塞) 线程因竞争同步锁(如 synchronized)被阻塞,等待锁释放
WAITING(等待) 线程通过 wait()join()LockSupport.park() 进入无时限等待,需其他线程唤醒
TIMED_WAITING(计时等待) 线程通过 sleep(long)wait(long)join(long) 进入有时限等待,超时后自动唤醒
TERMINATED(终止) 线程执行完 run() 方法或因异常退出,生命周期结束,不可再启动

2. 状态转换流程图

NEW → RUNNABLE:调用 thread.start()
RUNNABLE → BLOCKED:竞争 synchronized 锁失败
BLOCKED → RUNNABLE:获取 synchronized 锁
RUNNABLE → WAITING:调用 Object.wait() / Thread.join() / LockSupport.park()
WAITING → RUNNABLE:其他线程调用 Object.notify() / Object.notifyAll() / LockSupport.unpark()
RUNNABLE → TIMED_WAITING:调用 Thread.sleep(long) / Object.wait(long) / Thread.join(long)
TIMED_WAITING → RUNNABLE:超时自动唤醒 或 其他线程唤醒
RUNNABLE → TERMINATED:run() 执行完毕 或 线程抛出未捕获异常

注意:线程一旦进入 TERMINATED 状态,无法通过 start() 再次启动,否则会抛出 IllegalThreadStateException

五、线程中断

线程中断不是强制杀死线程,而是一种协作式通知机制

  • 给目标线程打一个中断标记interrupt flag = true
  • 线程自己决定何时、如何响应中断
  • Java 没有抢占式中断,一切都是协作

5.1、三个核心方法

方法 类型 作用 是否清除中断标记
thread.interrupt() 实例方法 发起中断,给线程设置中断标记 = true 不清除
isInterrupted() 实例方法 查询中断状态 不清除(标记保留)
Thread.interrupted() 静态方法 查询当前线程中断状态 自动清除(标记重置为 false)

5.2、方法详细说明

1. thread.interrupt()

  • 作用:中断目标线程
  • 本质:只是把线程的中断标记设为 true
  • 不会立刻停止线程,线程依然可以继续运行
  • 如果线程正在 sleep()/wait()/join(),会抛出 InterruptedException并清除中断标记

2. isInterrupted()

  • 实例方法,不清除中断标记
  • 用法:thread.isInterrupted()
  • 适合:只想查看状态,不想改变状态

3. Thread.interrupted()

  • 静态方法,自动清除中断标记
  • 调用一次后,中断标记变回 false
  • 适合:处理完中断后,希望重置状态

5.3、关键区别

Thread.interrupted() vs isInterrupted()

  1. 静态 vs 实例
    • interrupted():静态方法,只能查当前线程
    • isInterrupted():实例方法,可以查任意线程
  2. 是否清除标记
    • interrupted()会清除
    • isInterrupted()不会清除

5.4、中断的响应方式

线程收到中断后,有 3 种标准处理方式:

  1. 捕获 InterruptedException,处理后退出
  2. 检查中断标记,自行退出
  3. 不处理,继续运行(不推荐)

5.5、最佳实践代码示例

public class InterruptDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (!Thread.interrupted()) { // 会自动清除标记
                // 执行业务逻辑
                System.out.println("线程运行中...");
            }
            System.out.println("线程被中断,正常退出");
        });

        t.start();
        Thread.sleep(1000);
        t.interrupt(); // 发起中断
    }
}

5.6、最重要的一句话总结

Java 线程中断 = 协作式通知,不是强制停止;
interrupt() 设标记,isInterrupted() 查标记,interrupted() 查并清标记。


总结

  • 中断是标记,不是强制停止
  • 三个方法:发起、查询、查询+清除
  • Java 只有协作式中断,没有抢占式中断
Logo

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

更多推荐