并发 - Java 线程基础
并发编程前置 —— Java 线程基础
在学习线程池之前,我们必须先理解什么是线程,以及如何创建和使用它。
1. 核心理论:进程与线程
- 进程 (Process): 是操作系统进行资源分配和调度的基本单位。可以简单理解为一个正在运行的应用程序。比如你打开的微信、浏览器,每一个都是一个独立的进程。
- 线程 (Thread): 是进程的实际执行者,是 CPU 调度的最小单位。一个进程可以包含一个或多个线程。比如,你的浏览器进程里,可能有一个线程负责渲染页面,一个线程负责下载文件,一个线程负责播放音乐。
生活比喻:一个工厂就是一个进程,它拥有独立的土地、电力、原材料等资源。工厂里的工人就是一个个线程,他们共享工厂的资源,分工合作,真正完成生产任务。
2. 深度剖析:创建线程的两种方式
在 Java 中,创建新线程主要有两种方式。
方式一:继承 Thread 类
- 创建一个类,让它
extends Thread。 - 重写
run()方法,在run()方法中定义线程需要执行的任务逻辑。 - 创建这个子类的实例,并调用它的
start()方法来启动线程。
缺点:Java 是单继承的,如果你的类已经继承了另一个类,就不能再继承 Thread 类了,这种方式扩展性差。
方式二:实现 Runnable 接口 (推荐)
- 创建一个类,让它
implements Runnable。 - 实现
run()方法,定义任务逻辑。 - 创建一个该实现类的实例(我们称之为“任务”)。
- 创建一个
Thread对象,并将刚才的“任务”实例作为参数传给Thread的构造方法。 - 调用
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 线程的六种状态
-
NEW(新建)- 含义:线程刚刚被创建,但还没有调用
start()方法。它已经存在,但尚未启动。 - 特点:此时的线程只是一个
Thread对象,没有分配系统资源(如线程栈)。 - 生活比喻:一个刚被雇佣,但还没开始工作的工人。
- 含义:线程刚刚被创建,但还没有调用
-
RUNNABLE(可运行/运行中)- 含义:线程已经调用了
start()方法,处于就绪状态,正在等待操作系统调度 CPU 资源,或者正在 CPU 上运行。 - 特点:对于操作系统来说,
READY和RUNNING状态是区分的,但对于 JVM 来说,统称为RUNNABLE。线程在RUNNABLE状态下,随时可能被调度执行。 - 生活比喻:一个已经准备好随时可以工作的工人,他可能正在工作,也可能在等待被分配任务。
- 含义:线程已经调用了
-
BLOCKED(阻塞)- 含义:线程正在等待获取一个对象的监视器锁(
synchronized锁)。当一个线程尝试进入synchronized代码块或方法,但该锁已被其他线程持有时,它就会进入BLOCKED状态。 - 特点:线程不能执行任何操作,必须等待其他线程释放锁。
- 生活比喻:工人想进入一个已被其他工人占用的房间,只能在门口等着。
- 含义:线程正在等待获取一个对象的监视器锁(
-
WAITING(等待)- 含义:线程无限期地等待另一个线程执行特定操作。它会无限期地等待某个特定条件发生,例如等待
notify()或notifyAll()通知,或者等待另一个线程终止 (join())。 - 特点:线程会释放所持有的锁(如果持有)。
- 生活比喻:工人完成了自己的任务,等待经理(另一个线程)通知他开始下一个任务。他放下所有工具,在工位上等通知。
- 导致进入此状态的方法:
Object.wait(),Thread.join(),LockSupport.park()。
- 含义:线程无限期地等待另一个线程执行特定操作。它会无限期地等待某个特定条件发生,例如等待
-
TIMED_WAITING(有时限等待)- 含义:线程在指定的时间内等待另一个线程执行特定操作,或者等待一段时间后自动唤醒。
- 特点:线程会释放所持有的锁。
- 生活比喻:工人完成了自己的任务,等待经理通知,但只等 30 分钟。如果 30 分钟内没等到,就自己开始干别的事。
- 导致进入此状态的方法:
Thread.sleep(long millis),Object.wait(long timeout),Thread.join(long timeout),LockSupport.parkNanos(),LockSupport.parkUntil()。
-
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()等方法,会进入WAITING或TIMED_WAITING状态。
- 因为争抢
- 暂停的线程最终的目标都是回到
RUNNABLE状态,等待 CPU 再次调度:BLOCKED的线程在获取到锁之后,会回到RUNNABLE。WAITING的线程在被notify()或notifyAll()唤醒之后,也会回到RUNNABLE(但它还需要重新竞争锁)。TIMED_WAITING的线程在等待超时或被提前唤醒后,同样会回到RUNNABLE。
- 最后,当
run()方法执行完成,线程就会从RUNNABLE状态转为TERMINATED状态。
第三部分:补充关键细节以展示深度 (加分项)
为了展示我对这个知识点有更深入的理解,我还会补充几点关键的区别:
“尤其需要注意的是 BLOCKED、WAITING 和 TIMED_WAITING 这三个状态的区别:
- 触发原因不同:
BLOCKED是被动的,是 JVM 层面在获取synchronized锁失败时触发的;而WAITING和TIMED_WAITING是主动的,是开发者在代码中调用了wait()、sleep()等方法。 - 对锁的释放行为不同:这是一个重要的考点。
WAITING和TIMED_WAITING(由wait(time)引起时)状态是会释放已经持有的锁的。而BLOCKED状态是因为压根就没拿到锁。另外,由Thread.sleep()引起的TIMED_WAITING状态是不会释放任何锁的。”
通过这样层次分明、由浅入深的回答,就可以全面且清晰地向面试官展示你对这个知识点的掌握程度。

浙公网安备 33010602011771号