并发 - Java 线程基础

并发编程前置 —— Java 线程基础

在学习线程池之前,我们必须先理解什么是线程,以及如何创建和使用它。

1. 核心理论:进程与线程

  • 进程 (Process): 是操作系统进行资源分配和调度的基本单位。可以简单理解为一个正在运行的应用程序。比如你打开的微信、浏览器,每一个都是一个独立的进程。
  • 线程 (Thread): 是进程的实际执行者,是 CPU 调度的最小单位。一个进程可以包含一个或多个线程。比如,你的浏览器进程里,可能有一个线程负责渲染页面,一个线程负责下载文件,一个线程负责播放音乐。

生活比喻:一个工厂就是一个进程,它拥有独立的土地、电力、原材料等资源。工厂里的工人就是一个个线程,他们共享工厂的资源,分工合作,真正完成生产任务。


2. 深度剖析:创建线程的两种方式

在 Java 中,创建新线程主要有两种方式。

方式一:继承 Thread

  1. 创建一个类,让它 extends Thread
  2. 重写 run() 方法,在 run() 方法中定义线程需要执行的任务逻辑。
  3. 创建这个子类的实例,并调用它的 start() 方法来启动线程。

缺点:Java 是单继承的,如果你的类已经继承了另一个类,就不能再继承 Thread 类了,这种方式扩展性差。

方式二:实现 Runnable 接口 (推荐)

  1. 创建一个类,让它 implements Runnable
  2. 实现 run() 方法,定义任务逻辑。
  3. 创建一个该实现类的实例(我们称之为“任务”)。
  4. 创建一个 Thread 对象,并将刚才的“任务”实例作为参数传给 Thread 的构造方法。
  5. 调用 Thread 对象的 start() 方法。

优点:这种方式只是实现一个接口,不影响类的继承体系,更灵活。它将“线程”和“任务”解耦,是更受推荐的方式。

方式三:使用 Lambda 表达式 (Java 8+ 终极推荐)

由于 Runnable 是一个函数式接口(只有一个抽象方法 run()),从 Java 8 开始,我们可以使用 Lambda 表达式来极大地简化代码。

// 无需定义单独的 MyRunnable 类
Runnable task = () -> {
    System.out.println("我是一个 Lambda 表达式定义的任务!");
};
new Thread(task).start();

// 甚至可以更简洁
new Thread(() -> System.out.println("终极简化版!")).start();

优点:代码极其简洁、优雅,是目前创建简单任务的首选方式。


3. 生活中的例子与代码示例

  • 生活比喻:

    • Runnable 接口:就像一张工作指令单,上面写着需要完成的任务(比如“打扫卫生”)。它只是一张纸,自己不会动。
    • Thread 类:就像一个工人。你可以直接雇佣一个“专业打扫卫生的工人”(继承 Thread),也可以雇佣一个“普通工人”(new Thread()),然后把“打扫卫生”的指令单(Runnable 对象)交给他去执行。
    • Lambda 表达式:就像你口头告诉工人“去把地扫了”,连写指令单的功夫都省了。
  • 核心代码示例:

package com.study.concurrency;

public class ThreadCreation {

    // 方式一:继承 Thread 类
    static class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("方式一:通过继承 Thread 类创建的线程: " + getName());
        }
    }

    // 方式二:实现 Runnable 接口
    static class MyRunnable implements Runnable {
        @Override
        public void run() {
            System.out.println("方式二:通过 Runnable 接口创建的线程: " + Thread.currentThread().getName());
        }
    }

    public static void main(String[] args) throws InterruptedException {
        System.out.println("主线程开始: " + Thread.currentThread().getName());

        // --- 启动方式一的线程 ---
        new MyThread().start();

        // --- 启动方式二的线程 ---
        Thread threadWithTask = new Thread(new MyRunnable());
        threadWithTask.start();

        // --- 方式三:使用 Lambda 表达式 ---
        new Thread(() -> {
            System.out.println("方式三:使用 Lambda 创建的线程: " + Thread.currentThread().getName());
        }).start();

        Thread.sleep(100); // 等待子线程执行
        System.out.println("主线程结束。");
    }
}

4. start() vs run()

这是一个核心区别点:

  • t.start(): 启动一个的线程。这个新线程会处于就绪状态,等待 CPU 调度。当它被调度时,JVM 会自动调用该线程的 run() 方法。这是真正的并发。
  • t.run(): 不启动新线程。这只是一个普通的方法调用,它会在当前线程(比如 main 线程)中同步执行 run() 方法里的代码。

5. 线程的生命周期与状态

在 Java 中,一个线程从创建到死亡会经历 6 种状态。这些状态由 java.lang.Thread.State 枚举定义。

5.1 线程的六种状态

  1. NEW (新建)

    • 含义:线程刚刚被创建,但还没有调用 start() 方法。它已经存在,但尚未启动。
    • 特点:此时的线程只是一个 Thread 对象,没有分配系统资源(如线程栈)。
    • 生活比喻:一个刚被雇佣,但还没开始工作的工人。
  2. RUNNABLE (可运行/运行中)

    • 含义:线程已经调用了 start() 方法,处于就绪状态,正在等待操作系统调度 CPU 资源,或者正在 CPU 上运行。
    • 特点:对于操作系统来说,READYRUNNING 状态是区分的,但对于 JVM 来说,统称为 RUNNABLE。线程在 RUNNABLE 状态下,随时可能被调度执行。
    • 生活比喻:一个已经准备好随时可以工作的工人,他可能正在工作,也可能在等待被分配任务。
  3. BLOCKED (阻塞)

    • 含义:线程正在等待获取一个对象的监视器锁(synchronized 锁)。当一个线程尝试进入 synchronized 代码块或方法,但该锁已被其他线程持有时,它就会进入 BLOCKED 状态。
    • 特点:线程不能执行任何操作,必须等待其他线程释放锁。
    • 生活比喻:工人想进入一个已被其他工人占用的房间,只能在门口等着。
  4. WAITING (等待)

    • 含义:线程无限期地等待另一个线程执行特定操作。它会无限期地等待某个特定条件发生,例如等待 notify()notifyAll() 通知,或者等待另一个线程终止 (join())。
    • 特点:线程会释放所持有的锁(如果持有)。
    • 生活比喻:工人完成了自己的任务,等待经理(另一个线程)通知他开始下一个任务。他放下所有工具,在工位上等通知。
    • 导致进入此状态的方法Object.wait(), Thread.join(), LockSupport.park()
  5. TIMED_WAITING (有时限等待)

    • 含义:线程在指定的时间内等待另一个线程执行特定操作,或者等待一段时间后自动唤醒。
    • 特点:线程会释放所持有的锁
    • 生活比喻:工人完成了自己的任务,等待经理通知,但只等 30 分钟。如果 30 分钟内没等到,就自己开始干别的事。
    • 导致进入此状态的方法Thread.sleep(long millis), Object.wait(long timeout), Thread.join(long timeout), LockSupport.parkNanos(), LockSupport.parkUntil()
  6. TERMINATED (终止)

    • 含义:线程已经执行完毕,或者因异常退出,或者被停止。线程的生命周期结束。
    • 特点:线程对象不再处于活动状态,不能再次启动。
    • 生活比喻:工人完成了所有任务,下班回家了。

5.2 线程状态的转换

一个线程的生命周期就是其在这些状态之间转换的过程:

       start()                  (获取锁)             (超时或被中断)
NEW(新建) ---------> RUNNABLE(可运行/运行中) ---------> BLOCKED(阻塞) ---------> TIMED_WAITING(有时限等待) ---------> RUNNABLE
   ^            ^    |             ^    |              ^      |
   |            |    |             |    |              |      |
   |            |    v             |    v              |      v
   |            | WAITING(等待) <--------+----+--------------+------+-----------> TERMINATED
   |            |   ^                                       (run()方法执行完毕或异常退出)
   |            |   | notify()/notifyAll() / join()结束 / 超时
   |            +---+
(调度)

主要转换路径:
1. NEW -> RUNNABLE:调用 `start()` 方法。
2. RUNNABLE -> BLOCKED:尝试获取 `synchronized` 锁失败。
3. RUNNABLE -> WAITING:调用 `Object.wait()`, `Thread.join()`, `LockSupport.park()`。
4. RUNNABLE -> TIMED_WAITING:调用 `Thread.sleep(time)`, `Object.wait(time)`, `Thread.join(time)` 等。
5. BLOCKED/WAITING/TIMED_WAITING -> RUNNABLE:
   - BLOCKED:获取到锁。
   - WAITING:被 `notify()/notifyAll()` 唤醒,或 `join()` 线程结束。
   - TIMED_WAITING:等待时间结束,或被 `notify()/notifyAll()` 唤醒,或 `join()` 线程结束,或被中断。
6. RUNNABLE -> TERMINATED:`run()` 方法执行完毕,或方法中抛出未捕获的异常。
7. BLOCKED/WAITING/TIMED_WAITING -> TERMINATED:线程在等待状态下被中断且未处理异常,或者被停止。

5.3 梳理核心的状态转换路径

然后,我会串联起这些状态,说明它们之间是如何流转的:

  • 一个 NEW 状态的线程,通过调用 start() 方法,进入 RUNNABLE 状态。
  • 一个 RUNNABLE 状态的线程,可能会因为不同原因暂停执行:
    • 因为争抢 synchronized 锁失败,会进入 BLOCKED 状态。
    • 因为主动调用 Object.wait() 等方法,会进入 WAITINGTIMED_WAITING 状态。
  • 暂停的线程最终的目标都是回到 RUNNABLE 状态,等待 CPU 再次调度:
    • BLOCKED 的线程在获取到锁之后,会回到 RUNNABLE
    • WAITING 的线程在被 notify()notifyAll() 唤醒之后,也会回到 RUNNABLE(但它还需要重新竞争锁)。
    • TIMED_WAITING 的线程在等待超时或被提前唤醒后,同样会回到 RUNNABLE
  • 最后,当 run() 方法执行完成,线程就会从 RUNNABLE 状态转为 TERMINATED 状态。

第三部分:补充关键细节以展示深度 (加分项)

为了展示我对这个知识点有更深入的理解,我还会补充几点关键的区别:

“尤其需要注意的是 BLOCKEDWAITINGTIMED_WAITING 这三个状态的区别:

  • 触发原因不同BLOCKED 是被动的,是 JVM 层面在获取 synchronized 锁失败时触发的;而 WAITINGTIMED_WAITING 是主动的,是开发者在代码中调用了 wait()sleep() 等方法。
  • 对锁的释放行为不同:这是一个重要的考点。WAITINGTIMED_WAITING(由 wait(time) 引起时)状态是会释放已经持有的锁的。而 BLOCKED 状态是因为压根就没拿到锁。另外,由 Thread.sleep() 引起的 TIMED_WAITING 状态是不会释放任何锁的。”

通过这样层次分明、由浅入深的回答,就可以全面且清晰地向面试官展示你对这个知识点的掌握程度。

posted @ 2026-01-21 16:05  我是刘瘦瘦  阅读(0)  评论(0)    收藏  举报