|
@@ -0,0 +1,1439 @@
|
|
|
|
|
+# 20260810 课堂笔记 — 线程操作方法(Thread.sleep 线程休眠 + 线程调度方式(分时 / 抢占式调度模型)+ 多线程执行随机性(CPU 时间片)+ 线程优先级 getPriority / setPriority(默认 5、范围 1~10)+ Lambda 创建 Runnable 双线程并发演示)+ 守护线程(setDaemon 守护线程与 JVM 退出机制 + 守护线程特点(设置时机 / 继承性 / finally 不保证 / isDaemon)+ GC 垃圾回收线程经典场景)+ 线程生命周期(6 种状态:NEW / RUNNABLE / BLOCKED / WAITING / TIMED_WAITING / TERMINATED + Thread.State 枚举 + 状态转换枢纽 RUNNABLE + 操作系统 5 状态对比)+ 线程操作方法(join() / join(long) 等待线程结束 + interrupt() / isInterrupted() 中断标志机制 + sleep 中断抛异常并清除标志 + yield() 主动让出 CPU 时间片 + isAlive() 判断线程存活 + Object 类 wait() / notify() / notifyAll() 等待唤醒)+ 线程安全问题(进程间不共享 vs 线程间共享 + 数据竞争概念 + 卖票问题(相同票多次出现 / 负数票)+ 计数器问题(count++ 非原子性:读-改-写 3 步)+ 多线程安全问题 3 个原因(缺一不可)+ 线程安全三大特性(原子性 / 可见性 / 有序性)及解决方案)+ synchronized 线程同步(监视器锁 Monitor Lock + BLOCKED 排队等待 + 三种写法(同步实例方法锁 this / 同步静态方法锁 Class 对象 / 同步代码块锁指定对象)+ 锁粒度优化(只锁共享数据代码)+ 转账业务场景 + 加锁后 count 正确性验证)+ 随堂练习(三种创建线程方式回顾 + 线程常用方法清单 + CooperationTest join/interrupt/yield 综合)+ 课后作业回顾(实现 Callable 接口 + FutureTask 带返回值求和 + 三种实现方式适用场景思考)
|
|
|
|
|
+
|
|
|
|
|
+- **日期**:2026-08-10
|
|
|
|
|
+- **项目**:`c260810`
|
|
|
|
|
+- **包路径**:`course` / `exericse` / `homework0808`
|
|
|
|
|
+- **作者**:WanJL
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 目录
|
|
|
|
|
+
|
|
|
|
|
+1. [Thread.sleep() —— 线程休眠](#1-threadsleep--线程休眠)
|
|
|
|
|
+2. [线程调度方式:分时调度模型 vs 抢占式调度模型](#2-线程调度方式分时调度模型-vs-抢占式调度模型)
|
|
|
|
|
+3. [多线程执行的随机性:CPU 时间片机制](#3-多线程执行的随机性cpu-时间片机制)
|
|
|
|
|
+4. [线程优先级:getPriority() / setPriority()](#4-线程优先级getpriority--setpriority)
|
|
|
|
|
+5. [Demo01 综合演示:sleep + 优先级 + Lambda 双线程并发](#5-demo01-综合演示sleep--优先级--lambda-双线程并发)
|
|
|
|
|
+6. [守护线程:setDaemon() 与 JVM 退出机制(Demo02)](#6-守护线程setdaemon-与-jvm-退出机制demo02)
|
|
|
|
|
+7. [线程生命周期:6 种状态(Demo03)](#7-线程生命周期6-种状态demo03)
|
|
|
|
|
+8. [线程操作方法:join() / interrupt() / yield() / wait-notify(ThreadMethodDemo)](#8-线程操作方法join--interrupt--yield--wait-notifythreadmethoddemo)
|
|
|
|
|
+9. [线程安全问题:数据竞争与三大特性(Demo04)](#9-线程安全问题数据竞争与三大特性demo04)
|
|
|
|
|
+10. [synchronized 线程同步:三种写法与锁机制(SynchronizedDemo)](#10-synchronized-线程同步三种写法与锁机制synchronizeddemo)
|
|
|
|
|
+11. [随堂练习:三种创建线程方式回顾 + 线程常用方法(Exercise01)](#11-随堂练习三种创建线程方式回顾--线程常用方法exercise01)
|
|
|
|
|
+12. [课后作业回顾:Callable + FutureTask 带返回值求和(Homework_03)](#12-课后作业回顾callable--futuretask-带返回值求和homework_03)
|
|
|
|
|
+13. [知识点全景总结](#13-知识点全景总结)
|
|
|
|
|
+14. [随堂练习要点](#14-随堂练习要点)
|
|
|
|
|
+15. [拓展阅读](#15-拓展阅读)
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 1. Thread.sleep() —— 线程休眠
|
|
|
|
|
+
|
|
|
|
|
+### 概念 —— 让当前线程暂停执行
|
|
|
|
|
+
|
|
|
|
|
+`Thread.sleep(long millis)` 是操作线程的**静态方法**,作用是**线程休眠**:让**当前线程**休眠 `millis` 毫秒,休眠期间线程让出 CPU,时间到后恢复执行。
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+// 来源:course/Demo01.java(注释部分)
|
|
|
|
|
+/*
|
|
|
|
|
+ 操作线程的方法:
|
|
|
|
|
+
|
|
|
|
|
+ public static void sleep(long millis) 线程休眠 ,让当前线程休眠 millis 毫秒
|
|
|
|
|
+*/
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+> **要点**:
|
|
|
|
|
+> - **静态方法**:`Thread.sleep(毫秒)` 直接通过线程类名调用,作用是让**当前正在执行的线程**休眠
|
|
|
|
|
+> - **单位是毫秒**:`Thread.sleep(100)` 表示休眠 100 毫秒
|
|
|
|
|
+> - **必须处理中断异常**:`sleep()` 会抛受检异常 `InterruptedException`,调用处必须 try-catch 或 throws(Demo01 中用的是 try-catch + 抛 RuntimeException)
|
|
|
|
|
+
|
|
|
|
|
+### sleep 的经典使用场景
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+// 来源:course/Demo01.java(main 方法片段)
|
|
|
|
|
+Runnable run = () -> {
|
|
|
|
|
+ for (int i = 0; i < 100; i++) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ //休眠100毫秒
|
|
|
|
|
+ Thread.sleep(100);
|
|
|
|
|
+ } catch (InterruptedException e) {
|
|
|
|
|
+ throw new RuntimeException(e);
|
|
|
|
|
+ }
|
|
|
|
|
+ System.out.println(Thread.currentThread().getName() + "----->" + i);
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+> **要点**:
|
|
|
|
|
+> - sleep 放在循环里,让线程每隔 100 毫秒打印一次——**放慢执行速度**,方便观察多线程并发交替的效果
|
|
|
|
|
+> - 每次 sleep 都会**主动让出 CPU 时间片**,为其他线程创造执行机会,所以两条线程的输出会交替出现
|
|
|
|
|
+> - 中断异常用 try-catch 捕获后抛 `RuntimeException`(运行时异常),保证 Lambda 表达式无受检异常签名约束
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 2. 线程调度方式:分时调度模型 vs 抢占式调度模型
|
|
|
|
|
+
|
|
|
|
|
+### 概念 —— 操作系统如何决定"下一个执行哪个线程"
|
|
|
|
|
+
|
|
|
|
|
+线程调度就是**给线程分配 CPU 使用权**的规则。Java 中共有两种调度模型:
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+// 来源:course/Demo01.java(注释部分)
|
|
|
|
|
+/*
|
|
|
|
|
+ 线程调度方式:
|
|
|
|
|
+ 分时调度模型:所有的线程轮流获得CPU的使用权,平均分配每个线程占用CPU的时间片
|
|
|
|
|
+ 抢占式调度模型:优先让优先级高的线程使用CPU,如果优先级相同,则会随机选择一个,
|
|
|
|
|
+ 优先级高的线程获取的CPU时间片就更多一些。
|
|
|
|
|
+
|
|
|
|
|
+ Java使用的就是抢占式调度模型。
|
|
|
|
|
+*/
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 两种调度模型对比表
|
|
|
|
|
+
|
|
|
|
|
+| 调度模型 | 分配规则 | 优先级作用 | 典型系统 |
|
|
|
|
|
+|----------|----------|------------|----------|
|
|
|
|
|
+| **分时调度模型** | 所有线程**轮流**获得 CPU 使用权,**平均分配**时间片 | 基本不区分优先级 | 早期分时操作系统 |
|
|
|
|
|
+| **抢占式调度模型** | **优先让优先级高的线程**使用 CPU;优先级相同则**随机选择**一个 | 优先级高的线程获取的 CPU 时间片**更多** | **Java(默认采用)** |
|
|
|
|
|
+
|
|
|
|
|
+> **要点**:
|
|
|
|
|
+> - **分时调度**讲究"公平":大家轮流来、时间片均分,每个线程占用 CPU 的时间片平均分配
|
|
|
|
|
+> - **抢占式调度**讲究"择优":**优先级高的线程优先**获得 CPU,且拿到的**时间片更多**;如果两个线程优先级相同,就**随机选择一个**执行
|
|
|
|
|
+> - **Java 使用抢占式调度模型**——所以设置线程优先级(第 4 节)会影响线程获得 CPU 的机会
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 3. 多线程执行的随机性:CPU 时间片机制
|
|
|
|
|
+
|
|
|
|
|
+### 概念 —— 为什么多线程程序的输出顺序不确定
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+// 来源:course/Demo01.java(注释部分)
|
|
|
|
|
+/*
|
|
|
|
|
+ 所谓的随机性,其实就是假如计算机只有一个CPU,那么CPU在某一个时刻只能执行一个指令。
|
|
|
|
|
+ 线程只有得到了CPU时间片(使用权),才可以执行指令。所以多线程程序的执行是有随机性的,
|
|
|
|
|
+ 谁抢到CPU时间片不确定。
|
|
|
|
|
+*/
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 随机性产生的两个前提
|
|
|
|
|
+
|
|
|
|
|
+```
|
|
|
|
|
+① 单 CPU 时:某一时刻 CPU 只能执行一个指令
|
|
|
|
|
+② 线程只有抢到 CPU 时间片(使用权)才能执行指令
|
|
|
|
|
+→ 多个线程"谁先抢到时间片"不确定 → 执行顺序具有随机性
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+> **要点**:
|
|
|
|
|
+> - **单 CPU 同一时刻只能执行一条指令**——这是并发切换的硬件前提
|
|
|
|
|
+> - **线程必须拿到 CPU 时间片(使用权)才能执行指令**——拿不到就排队等待
|
|
|
|
|
+> - **谁抢到时间片不确定** → 多线程程序的**执行顺序具有随机性**,每次运行结果可能不同
|
|
|
|
|
+> - 这就是为什么"线程1 / 线程2 哪个先打印、打印多少"无法预判,只能观察到输出**交错混排**
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 4. 线程优先级:getPriority() / setPriority()
|
|
|
|
|
+
|
|
|
|
|
+### 概念 —— 通过优先级影响抢占调度的机会
|
|
|
|
|
+
|
|
|
|
|
+在抢占式调度模型下,**优先级高的线程**获取 CPU 时间片更多。Java 提供两个方法查看和修改线程优先级:
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+// 来源:course/Demo01.java(注释部分)
|
|
|
|
|
+/*
|
|
|
|
|
+ 设置线程的优先级:
|
|
|
|
|
+ public final int getPriority() 返回此线程的优先级
|
|
|
|
|
+ public final void setPriority(int newPriority) 修改此现场的优先级,线程默认优先级是5,范围是1~10
|
|
|
|
|
+*/
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 优先级规则速查
|
|
|
|
|
+
|
|
|
|
|
+| 项目 | 说明 |
|
|
|
|
|
+|------|------|
|
|
|
|
|
+| `int getPriority()` | 返回此线程的**优先级** |
|
|
|
|
|
+| `void setPriority(int newPriority)` | **修改**此线程的优先级 |
|
|
|
|
|
+| **默认优先级** | **5** |
|
|
|
|
|
+| **取值范围** | **1 ~ 10**(数值越大优先级越高) |
|
|
|
|
|
+
|
|
|
|
|
+> **要点**:
|
|
|
|
|
+> - **getPriority()**:实例方法,返回当前线程对象的优先级数值
|
|
|
|
|
+> - **setPriority(int)**:实例方法,修改线程的优先级,参数范围 **1~10**
|
|
|
|
|
+> - **默认优先级是 5**:new 出来的线程默认优先级为 5(如 Thread.NORM_PRIORITY)
|
|
|
|
|
+> - **优先级与调度模型联动**:优先级影响的是抢占式调度下**获得 CPU 的机率与时间片多少**,而不是"优先级高的线程一定先执行完"
|
|
|
|
|
+> - 超范围设置会抛 `IllegalArgumentException`(1~10 之外的取值非法)
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 5. Demo01 综合演示:sleep + 优先级 + Lambda 双线程并发
|
|
|
|
|
+
|
|
|
|
|
+### 演示目标
|
|
|
|
|
+
|
|
|
|
|
+把今天三个知识点串起来:用 **Lambda** 创建同一个 Runnable 任务,创建两个线程 t1 / t2,**打印默认优先级**,再用 `setPriority(8)` 修改 t1 的优先级后再次打印,最后 `start()` 启动两个线程观察并发执行:
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+// 来源:course/Demo01.java(main 方法完整逻辑)
|
|
|
|
|
+public static void main(String[] args) {
|
|
|
|
|
+ // Lambda 创建 Runnable 任务:休眠100毫秒后打印当前线程名和循环值
|
|
|
|
|
+ Runnable run = () -> {
|
|
|
|
|
+ for (int i = 0; i < 100; i++) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ Thread.sleep(100);
|
|
|
|
|
+ } catch (InterruptedException e) {
|
|
|
|
|
+ throw new RuntimeException(e);
|
|
|
|
|
+ }
|
|
|
|
|
+ System.out.println(Thread.currentThread().getName() + "----->" + i);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ Thread t1 = new Thread(run);
|
|
|
|
|
+ Thread t2 = new Thread(run);
|
|
|
|
|
+
|
|
|
|
|
+ // 打印默认优先级(两个线程都是5)
|
|
|
|
|
+ System.out.println(t1.getName() + "线程优先级:" + t1.getPriority());
|
|
|
|
|
+ System.out.println(t2.getName() + "线程优先级:" + t2.getPriority());
|
|
|
|
|
+
|
|
|
|
|
+ // 修改 t1 的优先级为8,再次打印对比
|
|
|
|
|
+ t1.setPriority(8);
|
|
|
|
|
+ System.out.println(t1.getName() + "线程优先级:" + t1.getPriority());
|
|
|
|
|
+ System.out.println(t2.getName() + "线程优先级:" + t2.getPriority());
|
|
|
|
|
+
|
|
|
|
|
+ t1.start();
|
|
|
|
|
+ t2.start();
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 运行流程拆解
|
|
|
|
|
+
|
|
|
|
|
+```
|
|
|
|
|
+1、Runnable run = () -> {...} 用 Lambda 创建任务(回顾方式二:Runnable)
|
|
|
|
|
+2、Thread t1 = new Thread(run) 两个线程共用同一个任务对象
|
|
|
|
|
+3、t1.getPriority() / t2.getPriority() 初始优先级都是 5(默认值)
|
|
|
|
|
+4、t1.setPriority(8) 把 t1 优先级改成 8,t2 仍为 5
|
|
|
|
|
+5、t1.start() / t2.start() 启动两个线程,run() 交替并发执行
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+> **要点**:
|
|
|
|
|
+> - **同一个 Runnable 两个线程**:t1 / t2 共用 `run` 这个 Lambda 任务——再次体会"任务与线程解耦"(Runnable 是任务,Thread 是线程承载者)
|
|
|
|
|
+> - **默认优先级验证**:初始 `getPriority()` 都返回 **5**,印证"线程默认优先级是 5"
|
|
|
|
|
+> - **setPriority 生效验证**:`t1.setPriority(8)` 后,t1 优先级变 8、t2 仍是 5——优先级是**每个线程独立**的属性
|
|
|
|
|
+> - **sleep + 并发观察**:两个线程各自休眠 100 毫秒后打印,输出会交错出现;t1 优先级更高,理论上抢占 CPU 的机会更多(但不会严格保证执行顺序)
|
|
|
|
|
+> - **线程名**:未指定名字时默认是 `Thread-0` / `Thread-1`,用 `Thread.currentThread().getName()` 获取
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 6. 守护线程:setDaemon() 与 JVM 退出机制(Demo02)
|
|
|
|
|
+
|
|
|
|
|
+### 概念 —— 什么是守护线程
|
|
|
|
|
+
|
|
|
|
|
+**守护线程**是随着**其他非守护线程的结束而结束**的线程。它与普通线程(用户线程)的区别,关键在于 **JVM 退出的条件**:
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+// 来源:course/Demo02.java(注释部分)
|
|
|
|
|
+/*
|
|
|
|
|
+ 守护线程 是随着其他非守护线程的结束而结束
|
|
|
|
|
+ public final void setDaemon(boolean on) 将当前线程标记为守护线程,当运行的线程都是守护线程的时候,JVM虚拟机会退出
|
|
|
|
|
+
|
|
|
|
|
+ JVM只有当所有的普通线程(用户线程)都结束的时候,才会退出。
|
|
|
|
|
+ 守护线程并不会阻碍JVM的退出。
|
|
|
|
|
+ 普通线程就是我们正常创建的线程,main主线程也是用户线程。只要有任何一个线程存活,JVM进程就不会退出。
|
|
|
|
|
+ 守护线程是属于服务型线程,专门为用户线程提供后台支持,当进程中只剩下守护线程的时候,JVM会直接结束,
|
|
|
|
|
+ 守护线程会被强制终止,甚至都来不及执行finally块的代码。
|
|
|
|
|
+*/
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 核心方法 —— setDaemon()
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+// 来源:course/Demo02.java(注释部分)
|
|
|
|
|
+public final void setDaemon(boolean on) // 将当前线程标记为守护线程
|
|
|
|
|
+public final boolean isDaemon() // 判断当前线程是否是守护线程
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 用户线程 vs 守护线程对照表
|
|
|
|
|
+
|
|
|
|
|
+| 对比维度 | 用户线程(普通线程) | 守护线程 |
|
|
|
|
|
+|----------|----------------------|----------|
|
|
|
|
|
+| **是否阻碍 JVM 退出** | **阻碍**——只要有任何一个用户线程存活,JVM 就不会退出 | **不阻碍**——当运行的线程都是守护线程时,JVM 退出 |
|
|
|
|
|
+| **角色定位** | 正常创建的线程(main 主线程也是用户线程) | **服务型线程**,专门为用户线程提供**后台支持** |
|
|
|
|
|
+| **被强制终止** | 正常执行完才结束 | 当只剩守护线程时 JVM 直接结束,守护线程被**强制终止**(连 finally 都来不及执行) |
|
|
|
|
|
+| **经典场景** | 业务逻辑(求和、打印、IO) | **GC 垃圾回收线程**、心跳检测、后台日志、监控统计 |
|
|
|
|
|
+
|
|
|
|
|
+> **要点**:
|
|
|
|
|
+> - **JVM 退出条件**:只有当**所有的用户线程(普通线程)都结束**时 JVM 才会退出;**只要还有一个用户线程存活,JVM 就不会退出**——这正是很多服务程序"卡着不退"的原因
|
|
|
|
|
+> - **守护线程不阻碍退出**:守护线程是服务型线程,专门为用户线程提供后台支持;当**只剩下守护线程**时,JVM 会**直接结束**,守护线程被**强制终止**
|
|
|
|
|
+> - **经典场景 GC 线程**:垃圾回收线程(GC Thread)就是最典型的守护线程——所有用户线程都执行完,GC 也就没有存在的意义,JVM 直接退出
|
|
|
|
|
+> - **其他场景**:心跳检测、后台日志、监控统计等"后台支撑型"任务都适合用守护线程
|
|
|
|
|
+
|
|
|
|
|
+### 守护线程的四大特点
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+// 来源:course/Demo02.java(注释部分)
|
|
|
|
|
+/*
|
|
|
|
|
+ 守护线程的特点:
|
|
|
|
|
+ 设置时机:守护线程必须要在执行start()方法前设置,否则会抛出异常:java.lang.IllegalThreadStateException
|
|
|
|
|
+ 继承性:守护线程创建的子线程也默认是守护线程
|
|
|
|
|
+ finally不保证:JVM退出的时候,守护线程会被强杀,finally不一定执行,不能用于资源清理
|
|
|
|
|
+ 使用场景:心跳检测、后台日志、监控统计等等
|
|
|
|
|
+ 判断是否是守护线程:public final boolean isDaemon()
|
|
|
|
|
+*/
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+| # | 特点 | 说明 |
|
|
|
|
|
+|---|------|------|
|
|
|
|
|
+| 1 | **设置时机** | `setDaemon(true)` 必须要在 `start()` **之前**调用,否则抛 `IllegalThreadStateException` |
|
|
|
|
|
+| 2 | **继承性** | 守护线程**创建的子线程也默认是守护线程**(继承守护属性) |
|
|
|
|
|
+| 3 | **finally 不保证** | JVM 退出时守护线程被**强杀**,`finally` **不一定执行**——**不能**用于资源清理 |
|
|
|
|
|
+| 4 | **判断方法** | `isDaemon()` 判断当前线程是否是守护线程 |
|
|
|
|
|
+
|
|
|
|
|
+> **要点**:
|
|
|
|
|
+> - **设置必须在 start() 之前**:`setDaemon(true)` 调用时机错误会抛 `IllegalThreadStateException`
|
|
|
|
|
+> - **守护属性会继承**:守护线程创建的子线程默认也是守护线程
|
|
|
|
|
+> - **强杀时 finally 不可靠**:守护线程在 JVM 退出时被强杀,`finally` 块不保证执行——所以**不要**用守护线程做资源清理(如关闭文件、释放连接)
|
|
|
|
|
+> - **适合后台支撑型任务**:心跳检测、后台日志、监控统计等"可有可无、随主线程而生灭"的任务
|
|
|
|
|
+
|
|
|
|
|
+### main 演示 —— 普通线程 + 守护线程对比
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+// 来源:course/Demo02.java(main 方法,本次更新修正设置时机)
|
|
|
|
|
+public static void main(String[] args) {
|
|
|
|
|
+ MyThread01 mt1 = new MyThread01();
|
|
|
|
|
+ MyThread01 mt2 = new MyThread01();
|
|
|
|
|
+ mt1.setName("线程1");
|
|
|
|
|
+ mt2.setName("守护线程");
|
|
|
|
|
+ // 设置线程为守护线程
|
|
|
|
|
+ // 普通线程执行完后,守护线程也就没有继续执行下去的必要了
|
|
|
|
|
+ mt2.setDaemon(true); // 必须在 start() 之前设置,否则抛 IllegalThreadStateException
|
|
|
|
|
+
|
|
|
|
|
+ mt1.start();
|
|
|
|
|
+ mt2.start();
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 演示用线程类 —— MyThread01(本次已抽取为独立文件)
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+// 来源:course/MyThread01.java(本次更新从 Demo02 内部类抽取为独立类)
|
|
|
|
|
+public class MyThread01 extends Thread {
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public void run() {
|
|
|
|
|
+ for (int i = 0; i < 100; i++) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ Thread.sleep(1000); // 每隔 1 秒打印一次,共 100 次(约 100 秒)
|
|
|
|
|
+ } catch (InterruptedException e) {
|
|
|
|
|
+ throw new RuntimeException(e);
|
|
|
|
|
+ }
|
|
|
|
|
+ System.out.println(this.getName() + "---->" + i);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 思考题 —— main 结束后 JVM 会退出吗
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+// 来源:course/Demo02.java(注释部分)
|
|
|
|
|
+/*
|
|
|
|
|
+ Q: JVM退出的条件是"所有的用户线程全部结束",如果main结束了,但是还有非守护线程的子线程在跑,JVM会退出吗?
|
|
|
|
|
+ A: 不会!只要还有一个用户线程存活,JVM就不会退出。也正是因为这个原因,所以会有很多服务程序,卡着不退出的情况。
|
|
|
|
|
+*/
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+> **要点**:
|
|
|
|
|
+> - **main 结束 ≠ JVM 退出**:只要还有一个**用户线程**存活,JVM 就不会退出——子线程会把 JVM"拖住"
|
|
|
|
|
+> - **服务程序卡住的原因**:很多后台服务程序"关不掉、卡着不退",往往就是还有用户线程在跑(如未停止的监听线程、任务线程)
|
|
|
|
|
+> - **设置时机修正(本次更新)**:代码更新前 `setDaemon(true)` 写在 `start()` **之后**,实际运行会抛 `IllegalThreadStateException`;本次已把 `setDaemon(true)` **移到 `start()` 之前**,保证守护线程设置合法生效——正对应"设置时机"特点
|
|
|
|
|
+> - **正确演示思路**:让"线程1"是普通用户线程(持续打印),让"守护线程"在用户线程结束后被 JVM 强制终止——直观体会"守护线程随用户线程结束而结束"
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 7. 线程生命周期:6 种状态(Demo03)
|
|
|
|
|
+
|
|
|
|
|
+### 概念 —— 线程从生到死的阶段
|
|
|
|
|
+
|
|
|
|
|
+**线程生命周期**就是**线程从生到死的过程**。直观上可分为 新建、就绪、运行、死亡 四个阶段,但**运行过程中**可能因为方法调用进入新的阶段——**阻塞**。实际上 Java 中线程的生命周期状态有 **6 种**,基于 **`Thread.State` 这个枚举类**:
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+// 来源:course/Demo03.java(注释部分)
|
|
|
|
|
+/*
|
|
|
|
|
+ 线程生命周期:也就是线程从生到死的过程,它可以分为这样几个阶段:
|
|
|
|
|
+ 新建、就绪、运行、死亡。
|
|
|
|
|
+ 但是在运行过程中,可能会因为其他方法调用,让线程出现一个新的阶段:阻塞
|
|
|
|
|
+
|
|
|
|
|
+ 实际上Java中线程的生命周期状态,有6种:
|
|
|
|
|
+ 新建--NEW
|
|
|
|
|
+ ...
|
|
|
|
|
+ 终止--TERMINATED
|
|
|
|
|
+ 基于Thread.state 这个枚举类
|
|
|
|
|
+*/
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 线程的 6 种状态速查表
|
|
|
|
|
+
|
|
|
|
|
+| 状态 | 枚举名 | 触发条件 | 说明 |
|
|
|
|
|
+|------|--------|----------|------|
|
|
|
|
|
+| **新建** | `NEW` | `new Thread()` | 已创建 Thread 对象,**还没调用 start()**,尚未与操作系统底层线程关联 |
|
|
|
|
|
+| **可运行(就绪)** | `RUNNABLE` | `start()` | 已调用 start(),**可能正在运行**,也可能在**等待 CPU 时间片**;已与底层线程关联,**完全由操作系统调度** |
|
|
|
|
|
+| **阻塞** | `BLOCKED` | 竞争同步锁失败 | **等待获取监视器锁进入同步块**,抢锁失败会被动阻塞,**不占用 CPU 时间** |
|
|
|
|
|
+| **等待** | `WAITING` | `wait()` / `join()` / `LockSupport.park()` | **无限期**等待另一个线程的通知,不占用 CPU 时间 |
|
|
|
|
|
+| **计时等待** | `TIMED_WAITING` | `sleep(ms)` / `wait(timeout)` / `join(timeout)` | **有明确等待时限**,超时自动恢复,不占用 CPU 时间 |
|
|
|
|
|
+| **终止** | `TERMINATED` | `run()` 方法结束 | run() 执行完毕或抛出**未捕获异常**而结束,线程终止后与底层线程**取消关联** |
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+// 来源:course/Demo03.java(注释部分)
|
|
|
|
|
+/*
|
|
|
|
|
+ 新建--NEW
|
|
|
|
|
+ |- 已经创建Thread对象,但是还没有调用start()方法,这个时候还没有和操作系统底层线程进行关联。
|
|
|
|
|
+ |- new Thread();
|
|
|
|
|
+ 可运行(就绪)--RUNNABLE
|
|
|
|
|
+ |- 已经调用了start()方法,可能正在运行,也可能在等待CPU时间片,已经开始和底层操作系统线程关联了。完全有操作系统进行调度。
|
|
|
|
|
+ |- start();
|
|
|
|
|
+ 阻塞--BLOCKED
|
|
|
|
|
+ |- 等待获取监视器锁进入同步块,抢锁失败会被动阻塞,不占用CPU时间。
|
|
|
|
|
+ |- 竞争同步锁失败
|
|
|
|
|
+ 等待--WAITING
|
|
|
|
|
+ |- 无限期的等待另一个线程的通知,不占用CPU时间
|
|
|
|
|
+ |- wait()
|
|
|
|
|
+ |- join();
|
|
|
|
|
+ |- LockSupport.park()
|
|
|
|
|
+ 计时等待--TIMED_WAITING
|
|
|
|
|
+ |- 有明确等待时限,超时自动回复,不占用CPU时间。
|
|
|
|
|
+ |- sleep(ms)
|
|
|
|
|
+ |- wait(timeout)
|
|
|
|
|
+ |- join(timeout)
|
|
|
|
|
+ 终止--TERMINATED
|
|
|
|
|
+ |- run()方法执行完毕,或抛出未捕获异常而结束,线程终止后会和底层线程取消关联。
|
|
|
|
|
+ |- run()方法结束
|
|
|
|
|
+*/
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+> **要点**:
|
|
|
|
|
+> - **NEW**:对象建好了但**没 start()**,此时还没和**操作系统底层线程**关联(纯粹是 Java 层的 Thread 对象)
|
|
|
|
|
+> - **RUNNABLE**:start() 之后**开始和底层线程关联**,**完全由操作系统调度**——它既包括"正在运行",也包括"排队等 CPU 时间片"(Java 把运行和就绪合并成一个 RUNNABLE)
|
|
|
|
|
+> - **BLOCKED**:`synchronized` 抢**监视器锁**失败被**被动阻塞**(后续线程同步课程会用到),不占用 CPU
|
|
|
|
|
+> - **WAITING vs TIMED_WAITING**:都是"等",区别是 WAITING **无限期**等(wait()/join()/LockSupport.park()),TIMED_WAITING **限时等**(sleep(ms)/wait(timeout)/join(timeout)),超时自动恢复
|
|
|
|
|
+> - **TERMINATED**:run() 执行完毕**或抛出未捕获异常**而结束,终止后与底层线程**取消关联**
|
|
|
|
|
+> - **第 1 节复习**:`Thread.sleep(ms)` 让线程进入的就是 **TIMED_WAITING(计时等待)** 状态——超时自动恢复
|
|
|
|
|
+
|
|
|
|
|
+### 状态转换规律 —— RUNNABLE 是总枢纽,TERMINATED 是终点站
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+// 来源:course/Demo03.java(注释部分)
|
|
|
|
|
+/*
|
|
|
|
|
+ RUNNABLE(可运行状态)是总枢纽,大多数状态都要回到RUNNABLE状态
|
|
|
|
|
+ 抢锁失败-->BLOCKED,拿到锁-->RUNNABLE
|
|
|
|
|
+ 主动休息-->TIMED_WAITING
|
|
|
|
|
+ 无线等待-->WAITING
|
|
|
|
|
+ 所有的阻塞状态都有办法回到RUNNABLE状态,只有TERMINATED状态是终点站。
|
|
|
|
|
+*/
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 状态转换图
|
|
|
|
|
+
|
|
|
|
|
+```
|
|
|
|
|
+ ┌───────────────┐
|
|
|
|
|
+ new Thread() │ │ run() 结束
|
|
|
|
|
+ │ ▼ ▼
|
|
|
|
|
+ ┌─────────┐ start() ┌─────────┐ TERMINATED(终点站)
|
|
|
|
|
+ │ NEW │ ──────────► │ RUNNABLE│
|
|
|
|
|
+ └─────────┘ └────┬────┘
|
|
|
|
|
+ ▲ │
|
|
|
|
|
+ │ ┌────────┼────────┐
|
|
|
|
|
+ │ ▼ ▼ ▼
|
|
|
|
|
+ │ ┌────────┐ ┌────────┐ ┌────────┐
|
|
|
|
|
+ │ │BLOCKED │ │WAITING │ │TIMED_ │
|
|
|
|
|
+ │ │抢锁失败 │ │wait() │ │WAITING │
|
|
|
|
|
+ │ │ │ │join() │ │sleep() │
|
|
|
|
|
+ │ └────────┘ └────────┘ └────────┘
|
|
|
|
|
+ │ ▲ ▲ ▲
|
|
|
|
|
+ │ └────────┼────────┘
|
|
|
|
|
+ │ 回到RUNNABLE
|
|
|
|
|
+ │ (阻塞状态都有办法回到RUNNABLE,只有TERMINATED是终点)
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+> **要点**:
|
|
|
|
|
+> - **RUNNABLE 是总枢纽**:大多数状态都要**回到 RUNNABLE**——抢锁失败→BLOCKED,拿到锁→RUNNABLE;主动休息→TIMED_WAITING;无限等待→WAITING
|
|
|
|
|
+> - **所有阻塞状态都能回到 RUNNABLE**:BLOCKED / WAITING / TIMED_WAITING 都有办法恢复执行
|
|
|
|
|
+> - **TERMINATED 是终点站**:线程一旦终止就**回不去了**——只有 TERMINATED 状态不可逆
|
|
|
|
|
+
|
|
|
|
|
+### 操作系统层面 vs Java 层面的状态划分
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+// 来源:course/Demo03.java(注释部分)
|
|
|
|
|
+/*
|
|
|
|
|
+ 操作系统层面一般把线程划分为5种状态:
|
|
|
|
|
+ 新建、就绪、运行、阻塞、终结
|
|
|
|
|
+*/
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+| 层面 | 状态数量 | 状态清单 |
|
|
|
|
|
+|------|----------|----------|
|
|
|
|
|
+| **操作系统层面** | 5 种 | 新建、就绪、**运行**、阻塞、终结 |
|
|
|
|
|
+| **Java(Thread.State)层面** | 6 种 | NEW、**RUNNABLE(就绪+运行合并)**、BLOCKED、WAITING、TIMED_WAITING、TERMINATED |
|
|
|
|
|
+
|
|
|
|
|
+> **要点**:
|
|
|
|
|
+> - **区别在于"运行/就绪"**:操作系统把"就绪"和"运行"分开(2 个状态);Java 把二者**合并成 RUNNABLE**(1 个状态)——因为 Java 无法精确区分"正在跑"还是"排队等 CPU"
|
|
|
|
|
+> - **Java 多出来的 WAITING / TIMED_WAITING**:这两个是 Java API 层面的"等待"细分,操作系统层统一归入"阻塞"
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 8. 线程操作方法:join() / interrupt() / yield() / wait-notify(ThreadMethodDemo)
|
|
|
|
|
+
|
|
|
|
|
+### 概念 —— join():让当前线程等待目标线程结束
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+// 来源:course/Demo03.java(注释部分,本次更新新增 yield / isAlive / wait / notify)
|
|
|
|
|
+/*
|
|
|
|
|
+ 线程操作方法:
|
|
|
|
|
+ public final void join() 让当前线程等待调用join()方法的那个线程执行完毕后再执行。
|
|
|
|
|
+ 比如: t.join(); 表示当前线程等待t结束。
|
|
|
|
|
+ public final void join(long millis) 最多等待指定毫秒,超过了就不等了
|
|
|
|
|
+ public void interrupt() 终端目标线程,只是设置一个中断标志,不会杀死线程,并且当目标线程处在join/wait/sleep的时候会抛异常,并且清除中断标志。
|
|
|
|
|
+ public static void yield() 让当前线程主动让出CPU时间片,重新参与竞争,只是建议,调度器不一定采纳。
|
|
|
|
|
+ public final boolean isAlive() 判断线程是否存活,已经执行了start()但是未结束
|
|
|
|
|
+ public static native Thread currentThread() 获取当前正在运行的线程对象
|
|
|
|
|
+
|
|
|
|
|
+ Object类的关于线程操作的方法:
|
|
|
|
|
+ 让线程等待
|
|
|
|
|
+ public final void wait()
|
|
|
|
|
+ public final void wait(long timeoutMillis)
|
|
|
|
|
+ public final void wait(long timeoutMillis, int nanos)
|
|
|
|
|
+ 唤醒线程
|
|
|
|
|
+ public final native void notify() 唤醒线程
|
|
|
|
|
+ public final native void notifyAll() 唤醒所有线程
|
|
|
|
|
+*/
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 线程操作方法速查表(本次更新扩展)
|
|
|
|
|
+
|
|
|
|
|
+| 方法 | 归属 | 作用 |
|
|
|
|
|
+|------|------|------|
|
|
|
|
|
+| `final void join()` | Thread | **当前线程等待**调用 join() 的那个线程**执行完毕后再执行**(如 `t.join()` 表示当前线程等待 t 结束) |
|
|
|
|
|
+| `final void join(long millis)` | Thread | **最多等待指定毫秒**,超过就不等了 |
|
|
|
|
|
+| `void interrupt()` | Thread | **中断**目标线程:只是**设置一个中断标志**,不会杀死线程;目标线程处在 join/wait/sleep 时会**抛异常**并**清除中断标志** |
|
|
|
|
|
+| `static void yield()` | Thread | 让当前线程**主动让出 CPU 时间片**,重新参与竞争——只是**建议**,调度器**不一定采纳** |
|
|
|
|
|
+| `final boolean isAlive()` | Thread | **判断线程是否存活**:已经执行了 `start()` 但**未结束** |
|
|
|
|
|
+| `static native Thread currentThread()` | Thread | 获取**当前正在运行**的线程对象 |
|
|
|
|
|
+| `final void wait()` | **Object** | **让线程等待**:无限期等待(配合 notify 使用) |
|
|
|
|
|
+| `final void wait(long timeoutMillis)` | Object | 让线程**限时等待**指定毫秒 |
|
|
|
|
|
+| `final void wait(long timeoutMillis, int nanos)` | Object | 让线程限时等待(毫秒 + 纳秒精度) |
|
|
|
|
|
+| `final native void notify()` | Object | **唤醒**一个正在等待的线程 |
|
|
|
|
|
+| `final native void notifyAll()` | Object | **唤醒所有**正在等待的线程 |
|
|
|
|
|
+
|
|
|
|
|
+> **要点**:
|
|
|
|
|
+> - **方法归属分两类**:`join` / `interrupt` / `yield` / `isAlive` / `currentThread` 是 **Thread 类**的线程操作方法;`wait` / `notify` / `notifyAll` 是 **Object 类**的线程操作方法(注意 `wait()` 不是 Thread 的方法)
|
|
|
|
|
+> - **yield 是"建议"**:`yield()` 主动让出 CPU 时间片只是**建议**,调度器**不一定采纳**(与抢占式调度模型呼应)
|
|
|
|
|
+> - **isAlive 判活**:执行了 `start()` 但未结束的线程算"存活";`TERMINATED` 后返回 false
|
|
|
|
|
+> - **wait/notify 依赖锁**:`wait()` / `notify()` 是线程**通信/同步**的基础(等待与唤醒配对),后续线程同步课程会深入
|
|
|
|
|
+
|
|
|
|
|
+### Demo03 main 演示 —— main 线程等待 mt2 线程结束
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+// 来源:course/Demo03.java(main 方法)
|
|
|
|
|
+public static void main(String[] args) throws InterruptedException {
|
|
|
|
|
+ MyThread01 mt1 = new MyThread01();
|
|
|
|
|
+ MyThread01 mt2 = new MyThread01();
|
|
|
|
|
+ MyThread01 mt3 = new MyThread01();
|
|
|
|
|
+ MyThread01 mt4 = new MyThread01();
|
|
|
|
|
+ mt1.setName("线程1");
|
|
|
|
|
+ mt2.setName("线程2");
|
|
|
|
|
+ mt3.setName("线程3");
|
|
|
|
|
+ mt4.setName("线程4");
|
|
|
|
|
+
|
|
|
|
|
+ mt1.start();
|
|
|
|
|
+ mt2.start();
|
|
|
|
|
+ mt3.start();
|
|
|
|
|
+ mt4.start();
|
|
|
|
|
+
|
|
|
|
|
+ for (int i = 0; i < 1000; i++) {
|
|
|
|
|
+ System.out.println(Thread.currentThread().getName() + "---->" + i);
|
|
|
|
|
+ }
|
|
|
|
|
+ //我们是在哪调用的join?是在main线程,也就是说
|
|
|
|
|
+ //当前的线程main线程,要等待mt2线程结束
|
|
|
|
|
+ mt2.join();
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+> **要点**:
|
|
|
|
|
+> - **4 个子线程 + main 并发**:mt1~mt4 各打印 0~99,main 打印 0~999——五条执行路径并发
|
|
|
|
|
+> - **join 的调用方是 main**:`mt2.join()` 写在 main 方法里,**当前线程 main 等待 mt2 线程结束**后再继续
|
|
|
|
|
+> - **join 与 sleep 的关系**:调用 join() 的线程(main)进入 **WAITING(等待)** 状态;join(timeout) 则进入 **TIMED_WAITING**(对应第 7 节状态表)
|
|
|
|
|
+> - **main 抛异常**:`throws InterruptedException`(join 是受检方法)
|
|
|
|
|
+
|
|
|
|
|
+### ThreadMethodDemo —— sleep / join / interrupt / yield 综合演示
|
|
|
|
|
+
|
|
|
|
|
+`ThreadMethodDemo` 把 sleep、join、interrupt、yield 四种线程操作方法做了**综合演示**,其中 sleep 与 join 部分被**注释保留**,活动代码演示 **interrupt() 中断机制** 与 **yield() 让出 CPU**:
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+// 来源:course/ThreadMethodDemo.java(注释部分,sleep / join 演示)
|
|
|
|
|
+//1、sleep()让当前线程休眠指定毫秒--TIME_WAITING 计时等待状态
|
|
|
|
|
+System.out.println(Thread.currentThread().getName() + "开始睡2秒.....");
|
|
|
|
|
+Thread.sleep(2000);
|
|
|
|
|
+System.out.println(Thread.currentThread().getName() + "睡醒了");
|
|
|
|
|
+
|
|
|
|
|
+//2、join()让当前线程等待指定线程结束后再继续
|
|
|
|
|
+Thread worker = new Thread(() -> {
|
|
|
|
|
+ for (int i = 0; i < 5; i++) {
|
|
|
|
|
+ System.out.println(Thread.currentThread().getName() + "工作中--->" + i);
|
|
|
|
|
+ try {
|
|
|
|
|
+ Thread.sleep(200);
|
|
|
|
|
+ } catch (InterruptedException e) {
|
|
|
|
|
+ Thread.currentThread().interrupt(); // 如果触发异常,就中断当前线程
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}, "worker-1");
|
|
|
|
|
+worker.start();
|
|
|
|
|
+worker.join(); // main线程会等待worker执行完成后继续执行
|
|
|
|
|
+
|
|
|
|
|
+Thread t = new Thread(() -> { ... sleep(5000) ... }, "worker-2");
|
|
|
|
|
+t.start();
|
|
|
|
|
+t.join(1000); // main线程会等待1秒,不等5秒
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### interrupt() 中断机制 —— 中断标志 + 主动检查
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+// 来源:course/ThreadMethodDemo.java(活动代码,interrupt 演示)
|
|
|
|
|
+//3、interrupt() 中断操作,设置中断标志
|
|
|
|
|
+Thread t1 = new Thread(() -> {
|
|
|
|
|
+ while (true) {
|
|
|
|
|
+ //主动检查是否中断--检查中断标志
|
|
|
|
|
+ if (Thread.currentThread().isInterrupted()) {
|
|
|
|
|
+ System.out.println("检查到中断标志...正常退出");
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ System.out.println("工作.......");
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ //如果线程正在被 sleep/wait的时候被中断会抛异常并清除标志
|
|
|
|
|
+ Thread.sleep(100);
|
|
|
|
|
+ } catch (InterruptedException e) {
|
|
|
|
|
+ System.out.println("sleep中被中断....");
|
|
|
|
|
+ //重新设置中断标志,方便上层能感知
|
|
|
|
|
+ Thread.currentThread().interrupt();
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}, "worker-3");
|
|
|
|
|
+
|
|
|
|
|
+t1.start();
|
|
|
|
|
+Thread.sleep(300); //让t1先跑一会儿
|
|
|
|
|
+t1.interrupt(); //向t1发送中断请求
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### interrupt 中断机制核心要点
|
|
|
|
|
+
|
|
|
|
|
+| 环节 | 说明 |
|
|
|
|
|
+|------|------|
|
|
|
|
|
+| **interrupt() 只是设标志** | `t1.interrupt()` **不会杀死线程**,只是**设置一个中断标志** |
|
|
|
|
|
+| **主动检查 isInterrupted()** | 线程内部用 `Thread.currentThread().isInterrupted()` **主动检查中断标志**,发现被中断则正常退出 |
|
|
|
|
|
+| **sleep/wait/join 中被中断** | 线程正处在 sleep / wait / join 时被 interrupt(),会**抛 InterruptedException** 并**清除中断标志** |
|
|
|
|
|
+| **catch 后重设标志** | catch 块里调用 `Thread.currentThread().interrupt()` **重新设置中断标志**,方便上层代码感知到中断 |
|
|
|
|
|
+
|
|
|
|
|
+> **要点**:
|
|
|
|
|
+> - **interrupt() 是"温柔"的中断**:只是**设置标志位**,不会像"杀线程"那样强制终止——线程需要**主动检查标志**或**在阻塞方法中收到异常**才会响应
|
|
|
|
|
+> - **两种响应方式**:①循环里用 `isInterrupted()` **主动检查**标志(有标志就 break 退出);②线程处于 sleep/wait/join 阻塞时被 interrupt(),会**抛 InterruptedException** 中断当前阻塞
|
|
|
|
|
+> - **清除标志的细节**:`InterruptedException` 抛出时**中断标志被清除**——所以 catch 里通常要**再调 interrupt() 重设标志**,让上层 `isInterrupted()` 能感知到中断状态
|
|
|
|
|
+> - **演示流程**:t1 启动后无限循环打印 → main `sleep(300)` 让 t1 先跑 → `t1.interrupt()` 发送中断请求 → t1 下次循环 `isInterrupted()` 检测到标志 → 正常退出
|
|
|
|
|
+> - **join(timeout) 场景**:`t.join(1000)` 表示 main **最多等 1 秒**,而 worker-2 要睡 5 秒——超时后 main 不等了,继续执行
|
|
|
|
|
+
|
|
|
|
|
+### interrupt 机制的补充说明(本次更新新增)
|
|
|
|
|
+
|
|
|
|
|
+`ThreadMethodDemo` 末尾补充了 interrupt 机制的**完整流程总结**,并新增了 `yield()` 的演示代码:
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+// 来源:course/ThreadMethodDemo.java(末尾注释,本次更新新增)
|
|
|
|
|
+/*
|
|
|
|
|
+ interrupt() 并不会杀死线程,而是设置一个中断标志,请求线程自行响应。
|
|
|
|
|
+ 如果线程处在sleep()/wait()/join()等阻塞状态,收到中断标志后,会抛出异常,并清除中断标志
|
|
|
|
|
+ 捕获异常后,应该重新调用interrupt()恢复中断标志,否则外层代码会感知不到中断
|
|
|
|
|
+ 线程自用interrupt()/Thread.interrupt()检查标志后,再决定是否退出。
|
|
|
|
|
+*/
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+> **要点**:
|
|
|
|
|
+> - **interrupt() 不杀线程**:只是设置中断标志,**请求线程自行响应**——是否退出由线程自己决定
|
|
|
|
|
+> - **阻塞状态响应**:线程处在 `sleep()` / `wait()` / `join()` 等阻塞状态时收到中断标志,会**抛异常**并**清除中断标志**
|
|
|
|
|
+> - **catch 后恢复标志**:捕获异常后应**重新调用 `interrupt()` 恢复中断标志**,否则**外层代码感知不到中断**
|
|
|
|
|
+> - **自检后决定退出**:线程用 `isInterrupted()` / `Thread.interrupted()` 检查标志后,再决定是否退出
|
|
|
|
|
+
|
|
|
|
|
+### yield() —— 主动让出 CPU 时间片(本次更新新增)
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+// 来源:course/ThreadMethodDemo.java(活动代码,yield 演示)
|
|
|
|
|
+//4、yield() 主动让出CPU时间片
|
|
|
|
|
+Thread t2 = new Thread(() -> {
|
|
|
|
|
+ for (int i = 0; i < 50; i++) {
|
|
|
|
|
+ Thread.yield(); // 主动让出CPU时间片,重新参与竞争
|
|
|
|
|
+ System.out.println(Thread.currentThread().getName() + "第" + i + "轮");
|
|
|
|
|
+ }
|
|
|
|
|
+}, "yield线程");
|
|
|
|
|
+
|
|
|
|
|
+t2.start();
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+> **要点**:
|
|
|
|
|
+> - **yield() 是静态方法**:`Thread.yield()` 让**当前线程**主动让出 CPU 时间片,**重新参与竞争**
|
|
|
|
|
+> - **只是建议**:`yield()` 只是**建议**调度器让出时间片,**调度器不一定采纳**——并发场景下别的线程可能立刻又抢到 CPU
|
|
|
|
|
+> - **yield 与 sleep 的区别**:`sleep(ms)` 是**限时休眠**(进入 TIMED_WAITING,时间到自动醒);`yield()` 是**让出一次**(回到 RUNNABLE,马上可能又被调度)——都不占用 CPU
|
|
|
|
|
+> - **演示逻辑**:yield 线程每轮先 `Thread.yield()` 再打印"第 i 轮",50 轮循环——让出时间片后与其他线程(如 main)交替执行
|
|
|
|
|
+
|
|
|
|
|
+### Object 类的线程操作方法 —— wait() / notify() / notifyAll()(本次更新新增)
|
|
|
|
|
+
|
|
|
|
|
+`Demo03` 新增了 **Object 类关于线程操作的方法**——这些方法属于**线程通信 / 同步**的基础(等待与唤醒配对):
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+// 来源:course/Demo03.java(注释部分,本次更新新增)
|
|
|
|
|
+/*
|
|
|
|
|
+ Object类的关于线程操作的方法:
|
|
|
|
|
+ 让线程等待
|
|
|
|
|
+ public final void wait()
|
|
|
|
|
+ public final void wait(long timeoutMillis)
|
|
|
|
|
+ public final void wait(long timeoutMillis, int nanos)
|
|
|
|
|
+ 唤醒线程
|
|
|
|
|
+ public final native void notify() 唤醒线程
|
|
|
|
|
+ public final native void notifyAll() 唤醒所有线程
|
|
|
|
|
+*/
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### wait / notify 方法速查表
|
|
|
|
|
+
|
|
|
|
|
+| 方法 | 作用 |
|
|
|
|
|
+|------|------|
|
|
|
|
|
+| `final void wait()` | **让线程等待**:无限期等待另一个线程的 notify / notifyAll |
|
|
|
|
|
+| `final void wait(long timeoutMillis)` | 让线程**限时等待**指定毫秒,超时自动恢复 |
|
|
|
|
|
+| `final void wait(long timeoutMillis, int nanos)` | 让线程限时等待(毫秒 + 纳秒精度) |
|
|
|
|
|
+| `final native void notify()` | **唤醒**一个正在等待的线程 |
|
|
|
|
|
+| `final native void notifyAll()` | **唤醒所有**正在等待的线程 |
|
|
|
|
|
+
|
|
|
|
|
+> **要点**:
|
|
|
|
|
+> - **归属 Object 而非 Thread**:`wait()` / `notify()` / `notifyAll()` 是 **Object 类**的方法——因为"等待/唤醒"针对的是**对象监视器(monitor)**,任何对象都可以作为锁对象调用
|
|
|
|
|
+> - **与线程状态呼应**:调用 `wait()` 的线程进入 **WAITING**(对应第 7 节状态表);`wait(timeout)` 进入 **TIMED_WAITING**
|
|
|
|
|
+> - **等待/唤醒配对**:`wait()` 让线程等待,`notify()` / `notifyAll()` 唤醒等待的线程——这是**生产者-消费者**等线程通信模式的基础
|
|
|
|
|
+> - **依赖锁**:`wait()` / `notify()` 必须在**持有对象锁**的同步块(synchronized)中调用,否则抛 `IllegalMonitorStateException`(后续线程同步课程深入)
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 9. 线程安全问题:数据竞争与三大特性(Demo04)
|
|
|
|
|
+
|
|
|
|
|
+### 概念 —— 为什么多线程会出问题(数据竞争)
|
|
|
|
|
+
|
|
|
|
|
+**进程和进程之间的数据和内存空间是不共享的、相互独立的;而一个进程中的多个线程之间的数据和内存是共享的**。正因为线程间共享数据,多线程带来的**最大隐患就是数据竞争**:
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+// 来源:course/Demo04.java(注释部分)
|
|
|
|
|
+/*
|
|
|
|
|
+ 我们知道进程和进程之间的数据和内存空间是不共享的,是相互独立的。
|
|
|
|
|
+ 一个进程中的多个线程之间的数据和内存是共享的。
|
|
|
|
|
+ 那么多线程所带来的最大的隐患就是:数据竞争。
|
|
|
|
|
+ 当多个线程同时读写同一个共享变量的时候,程序的执行结果将会变得不可预测。有时正确,有时错误。而且错误无法复现。
|
|
|
|
|
+*/
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+> **要点**:
|
|
|
|
|
+> - **进程间不共享**:进程与进程的数据、内存空间**相互独立**
|
|
|
|
|
+> - **线程间共享**:一个进程内的多个线程**共享数据和内存**
|
|
|
|
|
+> - **数据竞争**:多个线程**同时读写同一个共享变量**时,执行结果**不可预测**——有时正确、有时错误,且**错误无法复现**(这正是多线程 bug 最难排查的原因)
|
|
|
|
|
+
|
|
|
|
|
+### 亲手做一个 Bug —— 卖票问题(SellTicket)
|
|
|
|
|
+
|
|
|
|
|
+理解线程安全的第一步,就是**亲手制作一个 bug**:模拟 3 个售票窗口(3 个线程)同时卖 100 张票。
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+// 来源:course/SellTicket.java
|
|
|
|
|
+public class SellTicket implements Runnable {
|
|
|
|
|
+ private Integer tickets = 100; // 一共100张票
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public void run() {
|
|
|
|
|
+ while (true) {
|
|
|
|
|
+ if (tickets > 0) { // 多个线程可能同时通过这个判断
|
|
|
|
|
+ try {
|
|
|
|
|
+ Thread.sleep(10);
|
|
|
|
|
+ } catch (InterruptedException e) {
|
|
|
|
|
+ throw new RuntimeException(e);
|
|
|
|
|
+ }
|
|
|
|
|
+ System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
|
|
|
|
|
+ tickets--; // 减一张票
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+// 来源:course/Demo04.java(main 方法)
|
|
|
|
|
+public static void main(String[] args) {
|
|
|
|
|
+ SellTicket st = new SellTicket();
|
|
|
|
|
+ // 创建三个线程对象,模拟3个售票窗口
|
|
|
|
|
+ Thread t1 = new Thread(st, "窗口1");
|
|
|
|
|
+ Thread t2 = new Thread(st, "窗口2");
|
|
|
|
|
+ Thread t3 = new Thread(st, "窗口3");
|
|
|
|
|
+ t1.start();
|
|
|
|
|
+ t2.start();
|
|
|
|
|
+ t3.start();
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 卖票问题运行后的两个经典 Bug
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+// 来源:course/Demo04.java(注释部分)
|
|
|
|
|
+/*
|
|
|
|
|
+ 运行后看效果:
|
|
|
|
|
+ 1、相同的票出现了多次
|
|
|
|
|
+ 2、出现了负数的票
|
|
|
|
|
+ 原因:线程执行的随机性(抢占式调度)导致的,可能在卖票的过程中丢失了CPU的执行权,导致出现问题
|
|
|
|
|
+ 相同的票出现了多次:
|
|
|
|
|
+ 多个线程同时执行到if (tickets>0)判断通过,这个时候tickets还是同一个值,所以都是打印出 窗口3正在出售第10张票
|
|
|
|
|
+ 出现了负数的票:
|
|
|
|
|
+ tickets已经减到1的时候,多个线程同时通过if判断if (tickets>0),然后一个线程执行tickets--变成0,另一个线程继续执行tickets--变成-1
|
|
|
|
|
+ 程执行的随机性(抢占式调度)导致某个线程在"判断-输出-减票数"这三步的执行过程中,可能丢失CPU执行权,
|
|
|
|
|
+ 由另一个线程趁机插入到对共享数据的操作,破坏了数据的完整性。
|
|
|
|
|
+*/
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+| Bug | 现象 | 原因 |
|
|
|
|
|
+|-----|------|------|
|
|
|
|
|
+| **相同的票多次出现** | 多个线程都打印"窗口3正在出售第10张票" | 多个线程**同时通过 `if (tickets>0)` 判断**,此时 tickets 还是同一个值 |
|
|
|
|
|
+| **负数票** | tickets 减到 1 时出现 -1 | tickets 为 1 时多个线程同时通过判断,一个线程 `tickets--` 变 0,另一个再 `tickets--` 变 **-1** |
|
|
|
|
|
+
|
|
|
|
|
+> **要点**:
|
|
|
|
|
+> - **根因是随机性**:抢占式调度下,某个线程在"**判断 → 输出 → 减票数**"三步的执行过程中可能**丢失 CPU 执行权**,另一个线程趁机插入对共享数据(tickets)的操作,**破坏了数据的完整性**
|
|
|
|
|
+> - **"判断-操作"不是原子**:`if (tickets>0)` 和 `tickets--` 之间不是一气呵成的,线程随时可能被切换走
|
|
|
|
|
+
|
|
|
|
|
+### 计数器问题 —— count++ 不是原子操作(Counter)
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+// 来源:course/Counter.java
|
|
|
|
|
+public class Counter {
|
|
|
|
|
+ private Integer count = 0;
|
|
|
|
|
+
|
|
|
|
|
+ public void increment() {
|
|
|
|
|
+ count++; // 看似是1行,但底层是3步
|
|
|
|
|
+ //1、拿到当前count的值
|
|
|
|
|
+ //2、count+1
|
|
|
|
|
+ //3、自增后的值赋值给count
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public static void main(String[] args) throws InterruptedException {
|
|
|
|
|
+ Counter counter = new Counter();
|
|
|
|
|
+ int threadCount = 1000;
|
|
|
|
|
+
|
|
|
|
|
+ Thread[] threads = new Thread[threadCount];
|
|
|
|
|
+ for (int i = 0; i < threadCount; i++) {
|
|
|
|
|
+ threads[i] = new Thread(() -> counter.increment()); // 执行相加
|
|
|
|
|
+ threads[i].start();
|
|
|
|
|
+ }
|
|
|
|
|
+ //等待所有线程结束
|
|
|
|
|
+ for (Thread t : threads) {
|
|
|
|
|
+ t.join();
|
|
|
|
|
+ }
|
|
|
|
|
+ //理论上应该是1000,但是实际上往往小于1000
|
|
|
|
|
+ System.out.println("最终 count=" + counter.getCount());
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### count++ 的非原子性 —— 读-改-写三步
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+// 来源:course/Demo04.java(注释部分)
|
|
|
|
|
+/*
|
|
|
|
|
+ 计数器问题:
|
|
|
|
|
+ 理论上应该是1000,但是实际上少于1000。
|
|
|
|
|
+ 为什么结果不对?
|
|
|
|
|
+ 因为count++ 并不是原子操作(原子操作就是无论是多少步,都是一次性全部成功,要么全部失败回滚)
|
|
|
|
|
+ count++,表面上是一行代码,但在JVM中被拆成了3步:
|
|
|
|
|
+ 1、从内存读取count到寄存器(读)
|
|
|
|
|
+ 2、在寄存器中加1(改)
|
|
|
|
|
+ 3、把结果写会到内存(写)
|
|
|
|
|
+ 当多个线程同时执行 读-改-写 三步 的时候,就可能发生 读到了旧值,用旧值进行计算,覆盖别人写入的结果。
|
|
|
|
|
+ 导致丢失最新的结果
|
|
|
|
|
+*/
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+> **要点**:
|
|
|
|
|
+> - **count++ 不是原子操作**:原子操作指"无论多少步,**一次性全部成功或全部失败回滚**"
|
|
|
|
|
+> - **count++ 被拆成 3 步**:①**读**(内存 → 寄存器)②**改**(寄存器 +1)③**写**(结果写回内存)
|
|
|
|
|
+> - **丢失最新结果**:多个线程同时执行"读-改-写"三步时,可能**读到旧值、用旧值计算、覆盖别人写入的结果**——导致 `count` 理论 1000、实际**小于 1000**
|
|
|
|
|
+
|
|
|
|
|
+### 多线程安全问题引发的 3 个原因(缺一不可)
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+// 来源:course/Demo04.java(注释部分)
|
|
|
|
|
+/*
|
|
|
|
|
+ 多线程安全问题引发原因(缺一不可):
|
|
|
|
|
+ 1、多线程环境,至少两条路径在并发执行。
|
|
|
|
|
+ 2、存在共享数据,多个线程访问同一个变量/对象。
|
|
|
|
|
+ 3、有多条语句操作共享数据,并且这些语句之间存在 读-改-写 的符合操作,比如count++,比如ticket--等
|
|
|
|
|
+*/
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+| # | 原因 | 说明 |
|
|
|
|
|
+|---|------|------|
|
|
|
|
|
+| 1 | **多线程环境** | **至少两条执行路径**在并发执行 |
|
|
|
|
|
+| 2 | **存在共享数据** | 多个线程访问**同一个变量 / 对象** |
|
|
|
|
|
+| 3 | **读-改-写复合操作** | 有多条语句操作共享数据,且语句间存在**读-改-写**复合操作(如 `count++`、`tickets--`) |
|
|
|
|
|
+
|
|
|
|
|
+### 解决多线程安全问题的基本思想
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+// 来源:course/Demo04.java(注释部分)
|
|
|
|
|
+/*
|
|
|
|
|
+ 解决多线程安全性问题的基本方案思想:
|
|
|
|
|
+ 让程序不再具备产生安全问题的环境:
|
|
|
|
|
+ 1、把多条操作共享数据的语句锁起来。
|
|
|
|
|
+ 2、让任意时刻只有一个线程能执行这段代码,其他线程只能排队等待。
|
|
|
|
|
+*/
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+> **要点**:
|
|
|
|
|
+> - **思路是"消除产生安全问题的环境"**:不满足 3 个原因之一就不会出问题
|
|
|
|
|
+> - **方案一**:把多条操作共享数据的语句**锁起来**(synchronized / Lock)
|
|
|
|
|
+> - **方案二**:**任意时刻只有一个线程**能执行这段代码,其他线程**排队等待**
|
|
|
|
|
+
|
|
|
|
|
+### 线程安全三大特性(核心,必须背)
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+// 来源:course/Demo04.java(注释部分)
|
|
|
|
|
+/*
|
|
|
|
|
+ 线程安全的三大特性,必须背下来 多线程的核心
|
|
|
|
|
+
|
|
|
|
|
+ 原子性:操作不可分割,要么全部执行,要么全部不执行。防止 读-改-写 被并发打断。
|
|
|
|
|
+ |-解决方案:synchronized、Lock(锁)、Atomic类(原子类)
|
|
|
|
|
+ 可见性:一个线程的修改对另一个线程立即可见。防止线程把变量换成在CPU缓存中,其他线程看不到修改。
|
|
|
|
|
+ |-解决方案:volatile、synchronized、Lock(锁)
|
|
|
|
|
+ 有序性:代码按照书写顺序执行,不会被指令重排。防止编译器和CPU在执行的可能会对指令进行重排。
|
|
|
|
|
+ |-解决方案:volatile、synchronized
|
|
|
|
|
+ 原可序(原子性、可见性、有序性),但凡是线程安全方案,最终都是围绕这三个特性的。
|
|
|
|
|
+*/
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+| 特性 | 含义 | 解决的问题 | 解决方案 |
|
|
|
|
|
+|------|------|-----------|----------|
|
|
|
|
|
+| **原子性** | 操作**不可分割**,要么全部执行、要么全部不执行 | 防止"**读-改-写**"被并发打断 | `synchronized`、`Lock`(锁)、`Atomic` 类(原子类) |
|
|
|
|
|
+| **可见性** | 一个线程的**修改对另一个线程立即可见** | 防止线程把变量缓存到 CPU 缓存中,其他线程看不到修改 | `volatile`、`synchronized`、`Lock`(锁) |
|
|
|
|
|
+| **有序性** | 代码**按书写顺序执行**,不会被**指令重排** | 防止编译器和 CPU 对指令重排 | `volatile`、`synchronized` |
|
|
|
|
|
+
|
|
|
|
|
+> **要点**:
|
|
|
|
|
+> - **三大特性口诀**:原子性、可见性、有序性(记法"原可序")——**但凡线程安全方案,最终都是围绕这三个特性的**
|
|
|
|
|
+> - **原子性**:防"读-改-写"被打断 → 锁 / 原子类
|
|
|
|
|
+> - **可见性**:防 CPU 缓存导致的修改不可见 → volatile / 锁
|
|
|
|
|
+> - **有序性**:防指令重排 → volatile / synchronized
|
|
|
|
|
+> - 卖票问题和计数器问题正是**原子性**被破坏的典型案例(读-改-写三步被打断)
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 10. synchronized 线程同步:三种写法与锁机制(SynchronizedDemo)
|
|
|
|
|
+
|
|
|
|
|
+### 概念 —— synchronized 是保证原子性最简单可靠的手段
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+// 来源:course/Demo04.java(注释部分,本次更新新增)
|
|
|
|
|
+/*
|
|
|
|
|
+ 保证原子性:
|
|
|
|
|
+ 最简单的最可靠的同步手段保证原子性:synchronized(线程同步)
|
|
|
|
|
+ synchronized(线程同步)是Java内置的关键字,也是使用最广泛的同步手段。
|
|
|
|
|
+ 每个Java对象在底层都对应一把监视器锁(Monitor Lock),当线程进入到synchronized修饰的代码块之前,必
|
|
|
|
|
+ 须要先获取这个锁。如果这个锁已经被其他线程拿到了,那么当前现在就会进入到BLOCKED(阻塞)状态排队等待。
|
|
|
|
|
+ 持有锁的线程会执行完代码然后自动释放锁,在等待队列中的线程会重新开始进行新一轮的竞争。
|
|
|
|
|
+*/
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+| 项目 | 说明 |
|
|
|
|
|
+|------|------|
|
|
|
|
|
+| **synchronized 定位** | Java 内置关键字,保证原子性**最简单最可靠**的同步手段,使用最广泛 |
|
|
|
|
|
+| **监视器锁(Monitor Lock)** | 每个 Java 对象底层都对应一把监视器锁 |
|
|
|
|
|
+| **获取锁** | 线程进入 synchronized 代码块**之前必须先获取锁** |
|
|
|
|
|
+| **锁被占用** | 当前线程进入 **BLOCKED(阻塞)** 状态排队等待 |
|
|
|
|
|
+| **释放锁** | 持有锁的线程执行完代码**自动释放锁**,等待队列中的线程**重新竞争** |
|
|
|
|
|
+
|
|
|
|
|
+> **要点**:
|
|
|
|
|
+> - **synchronized 是关键字**:Java 内置的同步手段,不是方法,使用最广泛
|
|
|
|
|
+> - **每对象一把监视器锁**:`Monitor Lock` 是 synchronized 底层的锁机制
|
|
|
|
|
+> - **BLOCKED 状态呼应**:拿不到锁的线程进入 **BLOCKED**(对应第 7 节线程状态的"阻塞——等待获取监视器锁进入同步块")
|
|
|
|
|
+
|
|
|
|
|
+### synchronized 的优点与弊端
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+// 来源:course/Demo04.java(注释部分)
|
|
|
|
|
+/*
|
|
|
|
|
+ synchronized优点:
|
|
|
|
|
+ 解决了多线程的数据安全问题,保证了同一时刻只有一个线程能执行被保护的代码。
|
|
|
|
|
+ synchronized弊端:
|
|
|
|
|
+ 当线程很多的时候,每个线程都有去竞争同一把锁,比较耗资源,无形当中就会降低程序的运行效率。
|
|
|
|
|
+ 所以锁的粒度应该尽量小,只锁真正操作共享数据的代码,而不是整个方法。
|
|
|
|
|
+
|
|
|
|
|
+ synchronized修饰方法,该方法会变为同步方法。
|
|
|
|
|
+ synchronized修饰代码块,会变成同步代码块。
|
|
|
|
|
+*/
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+| 维度 | 说明 |
|
|
|
|
|
+|------|------|
|
|
|
|
|
+| **优点** | 解决多线程数据安全问题,保证**同一时刻只有一个线程**能执行被保护的代码 |
|
|
|
|
|
+| **弊端** | 线程多时**都去竞争同一把锁**比较耗资源,**降低程序运行效率** |
|
|
|
|
|
+| **优化方向** | **锁的粒度尽量小**——只锁真正操作共享数据的代码,而不是整个方法 |
|
|
|
|
|
+
|
|
|
|
|
+### synchronized 的三种写法(SynchronizedDemo)
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+// 来源:course/SynchronizedDemo.java
|
|
|
|
|
+public class SynchronizedDemo {
|
|
|
|
|
+ private Integer count = 0;
|
|
|
|
|
+
|
|
|
|
|
+ //写法1---同步实例方法,锁的是当前的对象 this
|
|
|
|
|
+ public synchronized void increment() {
|
|
|
|
|
+ count++;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ //写法2---同步静态方法(锁的是当前类的Class对象)
|
|
|
|
|
+ private static int staticCount = 0;
|
|
|
|
|
+ public static synchronized void staticIncrement() {
|
|
|
|
|
+ staticCount++;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ //写法3---同步代码块(锁的是指定的对象,最灵活)
|
|
|
|
|
+ private final Object lock = new Object();
|
|
|
|
|
+ public void increment03() {
|
|
|
|
|
+ synchronized (lock) { //只锁这一个代码块,粒度更细,性能更好
|
|
|
|
|
+ count++;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 三种写法对照表
|
|
|
|
|
+
|
|
|
|
|
+| 写法 | 示例 | 锁的是什么 | 特点 |
|
|
|
|
|
+|------|------|-----------|------|
|
|
|
|
|
+| **同步实例方法** | `public synchronized void increment()` | 当前对象 **this** | 锁住同一个对象的方法调用 |
|
|
|
|
|
+| **同步静态方法** | `public static synchronized void staticIncrement()` | 当前类 **Class 对象** | 锁住所有该类的静态调用,**全局唯一** |
|
|
|
|
|
+| **同步代码块** | `synchronized(lock) { ... }` | **指定的任意对象** obj | **最灵活**,只锁需要保护的部分,粒度最细、性能更好 |
|
|
|
|
|
+
|
|
|
|
|
+### synchronized 锁的到底是什么(Demo04 注释详解)
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+// 来源:course/Demo04.java(注释部分,本次更新新增)
|
|
|
|
|
+/*
|
|
|
|
|
+ synchronized锁的是什么?
|
|
|
|
|
+
|
|
|
|
|
+ public synchronized void method01(){
|
|
|
|
|
+ //假如300行代码,只有3行是修改共享数据的。那么先锁的是整个方法的300代码,粒度就太大了。
|
|
|
|
|
+ }
|
|
|
|
|
+ synchronized修饰的方法也就是同步方法。它锁的其实是当前的对象(this),锁住同一个对象的方法调用
|
|
|
|
|
+
|
|
|
|
|
+ public static synchronized void method01(){
|
|
|
|
|
+ //假如300行代码,只有3行是修改共享数据的。那么先锁的是整个方法的300代码,粒度就太大了。
|
|
|
|
|
+ }
|
|
|
|
|
+ synchronized修饰的静态方法也就是同步静态方法,它锁的其实是当前对象所属的类的Class对象。锁住的所有这个类的静态调用,全局唯一。
|
|
|
|
|
+
|
|
|
|
|
+ Object obj=new Object();
|
|
|
|
|
+ synchronized(obj){
|
|
|
|
|
+ //这个synchronized代码块只锁 那几行修改共享数据的代码,这样粒度就小很多。
|
|
|
|
|
+ }
|
|
|
|
|
+ synchronized修饰的代码块,锁的是指定的任意对象obj,更加灵活,只锁需要保护的部分。
|
|
|
|
|
+*/
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+// 来源:course/Demo04.java(新增方法示例)
|
|
|
|
|
+public synchronized void method01() {
|
|
|
|
|
+ //假如300行代码,只有3行是修改共享数据的。那么先锁的是整个方法的300代码,粒度就太大了。
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+public void method02() {
|
|
|
|
|
+ Object obj = new Object();
|
|
|
|
|
+ synchronized (obj) {
|
|
|
|
|
+ //这个synchronized代码块只锁 那几行修改共享数据的代码,这样粒度就小很多。
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+> **要点**:
|
|
|
|
|
+> - **同步实例方法锁 this**:锁的是**当前对象 this**,锁住同一个对象的方法调用
|
|
|
|
|
+> - **同步静态方法锁 Class 对象**:锁的是**当前类所属的 Class 对象**,全局唯一——锁住所有该类的静态调用
|
|
|
|
|
+> - **同步代码块锁指定对象**:锁的是**指定的任意对象 obj**,更灵活,只锁需要保护的部分
|
|
|
|
|
+> - **锁粒度意识**:一个方法 300 行只有 3 行改共享数据——同步方法会锁整个 300 行(粒度太大);同步代码块只锁那 3 行(粒度小、性能好)
|
|
|
|
|
+
|
|
|
|
|
+### 模拟业务场景 —— 转账(检查-修改必须整体锁住)
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+// 来源:course/SynchronizedDemo.java
|
|
|
|
|
+public class SynchronizedDemo {
|
|
|
|
|
+ //模拟业务场景--转账(多个变量要保持一致)
|
|
|
|
|
+ private int balance = 100;
|
|
|
|
|
+
|
|
|
|
|
+ public void transfer(int amount) {
|
|
|
|
|
+ //必须要把 检查-修改 这个过程锁住(同步)
|
|
|
|
|
+ synchronized (this) {
|
|
|
|
|
+ if (balance >= amount) {
|
|
|
|
|
+ balance -= amount;
|
|
|
|
|
+ System.out.println("转账" + amount + "成功,余额:" + balance);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ System.out.println("余额不足");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+> **要点**:
|
|
|
|
|
+> - **转账场景**:`if (balance >= amount)`(检查)+ `balance -= amount`(修改)**必须整体锁住**——否则多个线程同时"检查"通过后,余额可能被透支
|
|
|
|
|
+> - **synchronized(this)**:用当前对象作为锁,保护 balance 的"检查-修改"过程
|
|
|
|
|
+> - 这正对应第 9 节"解决基本思想":把操作共享数据的语句**锁起来**、同一时刻只有一个线程执行
|
|
|
|
|
+
|
|
|
|
|
+### main 演示 —— 加锁后 count 正确性验证
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+// 来源:course/SynchronizedDemo.java(main 方法)
|
|
|
|
|
+public static void main(String[] args) throws InterruptedException {
|
|
|
|
|
+ SynchronizedDemo sd = new SynchronizedDemo();
|
|
|
|
|
+
|
|
|
|
|
+ //1000个线程同时调用同步方法 调用的是同步方法,必然是1000
|
|
|
|
|
+ Thread[] threads = new Thread[1000];
|
|
|
|
|
+ for (int i = 0; i < 1000; i++) {
|
|
|
|
|
+ threads[i] = new Thread(sd::increment03);
|
|
|
|
|
+ threads[i].start();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ for (Thread t : threads) {
|
|
|
|
|
+ t.join();
|
|
|
|
|
+ }
|
|
|
|
|
+ System.out.println("加锁后最终 count=" + sd.count);
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+> **要点**:
|
|
|
|
|
+> - **与 Counter 对比**:第 9 节 `Counter` 用 1000 线程对**无锁**的 `count++` 并发,count 往往**小于 1000**;这里 1000 线程调用**加了 synchronized 的 `increment03()`**,count **必然是 1000**
|
|
|
|
|
+> - **join 等待全部线程**:`t.join()` 等所有线程执行完再打印结果
|
|
|
|
|
+> - **方法引用**:`sd::increment03` 是方法引用(回顾 08-04 函数式编程),等价于 `() -> sd.increment03()`
|
|
|
|
|
+> - **同一对象加锁才有效**:所有线程用的是**同一个 `sd` 对象**的 lock——若每个线程 new 一个 SynchronizedDemo,锁对象不同就失效了
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 11. 随堂练习:三种创建线程方式回顾 + 线程常用方法(Exercise01)
|
|
|
|
|
+
|
|
|
|
|
+### 练习要求 —— 把多线程三种实现方式与常用方法串起来
|
|
|
|
|
+
|
|
|
|
|
+`Exercise01` 是随堂练习的**骨架代码**,题目要求用三种方式创建并使用线程,并调用线程常用方法:
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+// 来源:exericse/Exercise01.java(注释部分)
|
|
|
|
|
+public static void main(String[] args) {
|
|
|
|
|
+ //1、使用继承Thread方式创建线程并使用线程
|
|
|
|
|
+ //2、使用runnable方式创建并使用线程
|
|
|
|
|
+ //3、使用callable方式创建并使用线程
|
|
|
|
|
+
|
|
|
|
|
+ /*
|
|
|
|
|
+ 调用一些方法
|
|
|
|
|
+ 获取当前线程名称的方法
|
|
|
|
|
+ 设置当前线程名称的方法
|
|
|
|
|
+ 获取线程返回结果的方法
|
|
|
|
|
+ */
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 三种创建线程方式速查表(回顾 20260808)
|
|
|
|
|
+
|
|
|
|
|
+| 方式 | 核心动作 | 返回结果 |
|
|
|
|
|
+|------|----------|----------|
|
|
|
|
|
+| **方式一:继承 Thread** | 自定义类 `extends Thread` + 重写 `run()` + `start()` 启动 | `run()` 无返回值 |
|
|
|
|
|
+| **方式二:实现 Runnable** | 实现类 `implements Runnable` + 重写 `run()` + `new Thread(runnable, "名")` | `run()` 无返回值 |
|
|
|
|
|
+| **方式三:实现 Callable** | 实现类 `implements Callable<V>` + 重写 `call()` + `new FutureTask<>(callable)` + `new Thread(futureTask)` | `call()` **有返回值** |
|
|
|
|
|
+
|
|
|
|
|
+### 练习要求调用的线程方法
|
|
|
|
|
+
|
|
|
|
|
+| 方法 | 归属 | 作用 |
|
|
|
|
|
+|------|------|------|
|
|
|
|
|
+| **获取当前线程名称** | `Thread.currentThread().getName()` | 静态方法拿当前线程对象,再取名字 |
|
|
|
|
|
+| **设置当前线程名称** | `thread.setName(String)` | 动态修改线程名 |
|
|
|
|
|
+| **获取线程返回结果** | `futureTask.get()` | 拿到 Callable 线程 `call()` 的返回值 |
|
|
|
|
|
+
|
|
|
|
|
+> **要点**:
|
|
|
|
|
+> - 这是把 20260808 学过的**三种多线程实现方式**做一次综合练习:继承 Thread、实现 Runnable、实现 Callable 各写一个线程任务
|
|
|
|
|
+> - 三个"常用方法"对应三种能力:**取线程名**(getName)、**改线程名**(setName)、**取返回结果**(get)——分别覆盖了方式二/三中"不继承 Thread 时如何定位线程"以及"Callable 如何拿返回值"的要点
|
|
|
|
|
+> - 与 Demo01 呼应:Demo01 已经用了 `Thread.currentThread().getName()`(取线程名)和 `setPriority()`(改优先级),练习还要补上 `setName()` 与 `get()`
|
|
|
|
|
+
|
|
|
|
|
+### 随堂练习完成版 —— CooperationTest(join + yield + interrupt 综合)
|
|
|
|
|
+
|
|
|
|
|
+`CooperationTest`(`exericse` 包)是本日**随堂练习的完成版**,把第 8 节的线程操作方法 join / yield / interrupt 串成一个完整演示——worker-1 用 `yield()` 让出 CPU 并配合 `join()` 等待,worker-2 用 `interrupt()` 中断长 sleep 任务:
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+// 来源:exericse/CooperationTest.java
|
|
|
|
|
+public static void main(String[] args) throws InterruptedException {
|
|
|
|
|
+ // ========== 1、join + yield 演示 ==========
|
|
|
|
|
+ Thread worker = new Thread(() -> {
|
|
|
|
|
+ for (int i = 0; i < 5; i++) {
|
|
|
|
|
+ Thread.yield(); // 主动让出 CPU 时间片
|
|
|
|
|
+ System.out.println(Thread.currentThread().getName() + "工作中--->" + i);
|
|
|
|
|
+ try {
|
|
|
|
|
+ Thread.sleep(200);
|
|
|
|
|
+ } catch (InterruptedException e) {
|
|
|
|
|
+ Thread.currentThread().interrupt(); // 重设中断标志
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }, "worker-1");
|
|
|
|
|
+
|
|
|
|
|
+ worker.start();
|
|
|
|
|
+ worker.join(); // 主线程等待 worker-1 结束
|
|
|
|
|
+ System.out.println(worker.getName() + "已结束,主线程继续...");
|
|
|
|
|
+
|
|
|
|
|
+ // ========== 2、interrupt 中断演示 ==========
|
|
|
|
|
+ Thread sleeper = new Thread(() -> {
|
|
|
|
|
+ try {
|
|
|
|
|
+ Thread.sleep(5000); // 长任务:睡 5 秒
|
|
|
|
|
+ System.out.println("任务完成"); // 被中断就不会执行到这
|
|
|
|
|
+ } catch (InterruptedException e) {
|
|
|
|
|
+ Thread.currentThread().interrupt(); // 重设中断标志
|
|
|
|
|
+ System.out.println("sleep 中被中断,isInterrupted()=" + Thread.interrupted());
|
|
|
|
|
+ }
|
|
|
|
|
+ }, "worker-2");
|
|
|
|
|
+
|
|
|
|
|
+ sleeper.start();
|
|
|
|
|
+ Thread.sleep(500); // 让 sleeper 先进入 sleep 状态
|
|
|
|
|
+ sleeper.interrupt(); // 向 worker-2 发送中断请求
|
|
|
|
|
+ sleeper.join(); // 主线程等 worker-2 处理完中断
|
|
|
|
|
+ System.out.println("主线程结束...");
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+> **要点**:
|
|
|
|
|
+> - **join 等待**:`worker.join()` 让主线程等 worker-1 跑完 5 轮再继续;`sleeper.join()` 等 worker-2 处理完中断
|
|
|
|
|
+> - **yield 让出**:worker-1 每轮先 `Thread.yield()` 主动让出 CPU,与其他线程交替执行
|
|
|
|
|
+> - **interrupt 中断**:worker-2 正在 sleep(5000) 长任务,主线程 sleep(500) 后 `interrupt()`,worker-2 在 sleep 中被中断抛 `InterruptedException`,catch 里重设标志后打印中断信息
|
|
|
|
|
+> - **细节注意**:代码中打印用的是 `Thread.interrupted()`(静态方法,检查并**清除**中断标志)——与 `isInterrupted()`(只检查不清除)对比理解
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 12. 课后作业回顾:Callable + FutureTask 带返回值求和(Homework_03)
|
|
|
|
|
+
|
|
|
|
|
+### 作业背景
|
|
|
|
|
+
|
|
|
|
|
+`homework0808/Homework_03.java`(本次由 `homework_03` 重命名、类名规范化为 `Homework_03`,逻辑不变)是 8 月 8 日课后作业的第三题:**用方式三(实现 Callable 接口 + FutureTask)创建 3 个线程,分别求 1~100 / 1~1000 / 1~10000 的和,并通过 `get()` 获取返回值**。与本日的"线程操作方法、三种实现方式综合练习"呼应。
|
|
|
|
|
+
|
|
|
|
|
+### ① 实现 Callable 接口:泛型指定返回类型
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+// 来源:homework0808/Homework_03.java
|
|
|
|
|
+static class SumCallable implements Callable<Integer> {
|
|
|
|
|
+ private int n; // 求 1~n 的和
|
|
|
|
|
+
|
|
|
|
|
+ public SumCallable(int n) {
|
|
|
|
|
+ this.n = n;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public Integer call() throws Exception {
|
|
|
|
|
+ int sum = 0;
|
|
|
|
|
+ for (int i = 1; i <= n; i++) {
|
|
|
|
|
+ sum += i;
|
|
|
|
|
+ }
|
|
|
|
|
+ System.out.println(Thread.currentThread().getName() + "求和结果:" + sum);
|
|
|
|
|
+ return sum; // 返回计算结果(get() 会拿到这个值)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+> **要点**:
|
|
|
|
|
+> - **`Callable<Integer>` 泛型** = `call()` 的返回值类型(求 int 和所以用 `Integer` 包装类)
|
|
|
|
|
+> - **`call()` 是带返回值**的线程任务方法,且声明 `throws Exception`(对比 Runnable 的 `run()` 无返回值)
|
|
|
|
|
+> - **任务携带参数**:`SumCallable(int n)` 通过构造方法把求和上限 n 传入任务对象——任务对象可以"携带数据"
|
|
|
|
|
+> - **`Thread.currentThread().getName()`**:方式三不继承 Thread,用静态方法获取当前线程名
|
|
|
|
|
+
|
|
|
|
|
+### ② FutureTask 包装 + 3 个线程启动 + get() 获取返回值
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+// 来源:homework0808/Homework_03.java(main 方法核心逻辑)
|
|
|
|
|
+public static void main(String[] args) throws Exception {
|
|
|
|
|
+ // 1、创建 3 个 SumCallable 对象(求 1~100 / 1~1000 / 1~10000 的和)
|
|
|
|
|
+ SumCallable sc1 = new SumCallable(100);
|
|
|
|
|
+ SumCallable sc2 = new SumCallable(1000);
|
|
|
|
|
+ SumCallable sc3 = new SumCallable(10000);
|
|
|
|
|
+
|
|
|
|
|
+ // 2、用 FutureTask<Integer> 包装每个 Callable 对象
|
|
|
|
|
+ FutureTask<Integer> ft1 = new FutureTask<>(sc1);
|
|
|
|
|
+ FutureTask<Integer> ft2 = new FutureTask<>(sc2);
|
|
|
|
|
+ FutureTask<Integer> ft3 = new FutureTask<>(sc3);
|
|
|
|
|
+
|
|
|
|
|
+ // 3、new Thread(futureTask, "线程名") 创建 3 个线程并 start()
|
|
|
|
|
+ Thread t1 = new Thread(ft1, "线程1");
|
|
|
|
|
+ Thread t2 = new Thread(ft2, "线程2");
|
|
|
|
|
+ Thread t3 = new Thread(ft3, "线程3");
|
|
|
|
|
+ t1.start();
|
|
|
|
|
+ t2.start();
|
|
|
|
|
+ t3.start();
|
|
|
|
|
+
|
|
|
|
|
+ // 4、调用 ft.get() 获取每个线程的返回值,打印并在 main 中汇总总结果
|
|
|
|
|
+ System.out.println(ft1.get());
|
|
|
|
|
+ System.out.println(ft2.get());
|
|
|
|
|
+ System.out.println(ft3.get());
|
|
|
|
|
+
|
|
|
|
|
+ // 5、(思考)对比三种实现方式:run() 无返回值 vs call() 有返回值,各自适合什么场景
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 完整调用链
|
|
|
|
|
+
|
|
|
|
|
+```
|
|
|
|
|
+SumCallable(100) → FutureTask<Integer>(sc1) → new Thread(ft1, "线程1") → t1.start()
|
|
|
|
|
+ ↓ 异步执行 call()
|
|
|
|
|
+ft1.get() ←————— 等待线程1计算完成,返回 call() 的返回值(5050)
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+> **要点**:
|
|
|
|
|
+> - **FutureTask 是"中间人"**:Callable 不能直接交给 Thread,用 `FutureTask<Integer>` 包装后传给 Thread 构造方法(Thread 构造只接收 Runnable / FutureTask)
|
|
|
|
|
+> - **每个线程独立求和**:sc1/sc2/sc3 三个任务对象,分别算 1~100、1~1000、1~10000 的和,各自包装成 ft1/ft2/ft3
|
|
|
|
|
+> - **指定线程名**:`new Thread(ft1, "线程1")` 第二个参数指定线程名,`call()` 里 `Thread.currentThread().getName()` 就能拿到它
|
|
|
|
|
+> - **get() 阻塞等待 + 拿返回值**:`ft1.get()` 会等待线程1计算完成,返回其 `call()` 的返回值(5050),打印结果
|
|
|
|
|
+> - **main 抛异常**:`throws Exception` 统一处理 `call()` / `get()` 抛出的受检异常
|
|
|
|
|
+> - **三种方式对比(作业思考题)**:`run()` 无返回值,适合只执行任务、不需要结果;`call()` 有返回值,适合"计算完要把结果拿回来用"的场景(如求和、查询、累加)
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 13. 知识点全景总结
|
|
|
|
|
+
|
|
|
|
|
+本课知识点围绕"**线程操作方法 + 守护线程 + 线程生命周期 + 线程安全问题 + synchronized 线程同步 + 多线程实现方式综合应用**"展开,一张表看清"知识 → 代码位置":
|
|
|
|
|
+
|
|
|
|
|
+| 知识点 | 具体体现 | 源码位置 |
|
|
|
|
|
+|--------|----------|----------|
|
|
|
|
|
+| **Thread.sleep(毫秒)** | 静态方法,让当前线程休眠指定毫秒数;抛 InterruptedException 需处理 | course/Demo01 |
|
|
|
|
|
+| **sleep 的应用** | 循环中每 100 毫秒打印一次,放慢执行、让出时间片,便于观察并发交替 | course/Demo01 main |
|
|
|
|
|
+| **线程调度方式** | 分时调度(轮流 + 平均分配时间片)vs 抢占式调度(优先高优先级、同优先级随机选);**Java 使用抢占式调度** | course/Demo01 注释 |
|
|
|
|
|
+| **多线程执行的随机性** | 单 CPU 同一时刻只能执行一条指令;线程抢到时间片才执行;谁抢到不确定 → 输出顺序随机 | course/Demo01 注释 |
|
|
|
|
|
+| **getPriority()** | 返回此线程的优先级(默认 5) | course/Demo01 |
|
|
|
|
|
+| **setPriority(int)** | 修改线程优先级,范围 1~10 | course/Demo01 |
|
|
|
|
|
+| **优先级与调度联动** | 抢占式调度下优先级高的线程获取 CPU 时间片更多 | course/Demo01 注释 |
|
|
|
|
|
+| **Lambda 创建 Runnable** | `Runnable run = () -> {...}` 作为任务对象,可同时给多个 Thread 使用(回顾方式二) | course/Demo01 main |
|
|
|
|
|
+| **双线程并发演示** | 同一个 Runnable 创建 t1 / t2 两个线程,start() 后 run() 交替并发执行 | course/Demo01 main |
|
|
|
|
|
+| **线程默认名** | 未指定名字时默认 Thread-0 / Thread-1 | course/Demo01 main |
|
|
|
|
|
+| **守护线程概念** | 守护线程是随其他非守护线程结束而结束的服务型线程;用户线程全部结束 JVM 才退出 | course/Demo02 注释 |
|
|
|
|
|
+| **setDaemon(boolean)** | 将当前线程标记为守护线程;当运行的线程都是守护线程时 JVM 退出 | course/Demo02 注释 |
|
|
|
|
|
+| **isDaemon()** | 判断当前线程是否是守护线程 | course/Demo02 注释 |
|
|
|
|
|
+| **JVM 退出机制** | 只要有任何一个用户线程存活,JVM 就不退出(main 结束 ≠ JVM 退出)——服务程序"卡着不退"的原因 | course/Demo02 注释 |
|
|
|
|
|
+| **守护线程经典场景** | GC 垃圾回收线程最典型;心跳检测、后台日志、监控统计等后台支撑任务 | course/Demo02 注释 |
|
|
|
|
|
+| **守护线程四大特点** | ①setDaemon 必须在 start() 前(否则 IllegalThreadStateException)②守护线程创建的子线程默认也是守护线程 ③JVM 强杀时 finally 不一定执行、不能用于资源清理 ④isDaemon() 判断 | course/Demo02 注释 |
|
|
|
|
|
+| **守护线程演示** | MyThread01(sleep 1000ms 循环打印)创建"线程1"普通线程 + "守护线程";`setDaemon(true)` **在 start() 之前**设置(本次更新修正,保证合法生效) | course/Demo02 main |
|
|
|
|
|
+| **MyThread01/02 抽取独立文件** | 原 Demo02 内部静态类抽取为独立外部类 `MyThread01` / `MyThread02`(代码重构,逻辑不变) | course/MyThread01、MyThread02 |
|
|
|
|
|
+| **线程生命周期概念** | 线程从生到死的过程:新建、就绪、运行、死亡,运行中可能进入阻塞 | course/Demo03 注释 |
|
|
|
|
|
+| **线程 6 种状态(Thread.State 枚举)** | NEW(new 未 start)/ RUNNABLE(start 后运行或等时间片)/ BLOCKED(抢监视器锁失败)/ WAITING(wait/join/park 无限期等)/ TIMED_WAITING(sleep/wait(timeout)/join(timeout) 限时等)/ TERMINATED(run 结束) | course/Demo03 注释 |
|
|
|
|
|
+| **状态转换规律** | RUNNABLE 是**总枢纽**(阻塞状态都有办法回到 RUNNABLE);TERMINATED 是**终点站**(不可逆) | course/Demo03 注释 |
|
|
|
|
|
+| **操作系统 5 状态 vs Java 6 状态** | OS 分新建/就绪/运行/阻塞/终结 5 种;Java 把就绪+运行合并为 RUNNABLE,且细分 WAITING / TIMED_WAITING | course/Demo03 注释 |
|
|
|
|
|
+| **join() / join(long)** | `t.join()` 当前线程等待 t 执行完毕;`join(millis)` 最多等待指定毫秒(超过不等)——调用方进入 WAITING / TIMED_WAITING | course/Demo03、ThreadMethodDemo |
|
|
|
|
|
+| **interrupt() 中断机制** | 只是**设置中断标志**不杀线程;`isInterrupted()` 主动检查;sleep/wait/join 中被中断抛 InterruptedException 并**清除标志**,catch 后通常**重设标志** | course/ThreadMethodDemo |
|
|
|
|
|
+| **interrupt 机制补充说明** | interrupt 请求线程**自行响应**;阻塞状态收到中断标志抛异常并清除标志;catch 后**重新 interrupt() 恢复标志**;线程自检标志后再决定退出 | course/ThreadMethodDemo 末尾注释 |
|
|
|
|
|
+| **yield() 主动让出 CPU** | 静态方法,让当前线程主动让出 CPU 时间片重新竞争;**只是建议**调度器不一定采纳;与 sleep 区别(yield 让一次回到 RUNNABLE,sleep 限时进 TIMED_WAITING) | course/Demo03 注释、ThreadMethodDemo |
|
|
|
|
|
+| **isAlive() 判断存活** | 判断线程是否存活:已执行 start() 但未结束;TERMINATED 后返回 false | course/Demo03 注释 |
|
|
|
|
|
+| **Object 类线程方法 wait/notify** | `wait()` / `wait(timeout)` / `wait(timeout,nanos)` 让线程等待(进 WAITING / TIMED_WAITING);`notify()` 唤醒一个 / `notifyAll()` 唤醒所有——线程通信基础,依赖对象锁 | course/Demo03 注释 |
|
|
|
|
|
+| **Demo03 main 演示** | 4 个 MyThread01 线程 + main 并发打印,最后 `mt2.join()` 让 main 等待线程2结束 | course/Demo03 main |
|
|
|
|
|
+| **ThreadMethodDemo 综合演示** | sleep(TIMED_WAITING)/ join(main 等 worker)/ join(1000)(最多等 1 秒)/ interrupt(t1 循环检查标志退出)/ yield(yield 线程每轮先让出 CPU 再打印) | course/ThreadMethodDemo |
|
|
|
|
|
+| **数据竞争概念** | 进程间数据不共享、线程间共享;多线程同时读写同一共享变量 → 结果不可预测、错误无法复现 | course/Demo04 注释 |
|
|
|
|
|
+| **卖票问题(SellTicket)** | 3 窗口线程卖 100 张票;相同票多次出现(多线程同时通过 if 判断)+ 负数票(tickets-- 连续执行) | course/SellTicket、Demo04 main |
|
|
|
|
|
+| **计数器问题(Counter)** | count++ 不是原子操作,JVM 拆成"读-改-写"3 步;多线程并发时读到旧值覆盖新值,理论 1000 实际小于 1000 | course/Counter |
|
|
|
|
|
+| **多线程安全问题 3 原因** | ①多线程环境 ②存在共享数据 ③多条语句操作共享数据且有读-改-写复合操作(缺一不可) | course/Demo04 注释 |
|
|
|
|
|
+| **解决基本思想** | 把操作共享数据的语句**锁起来**;**任意时刻只有一个线程**执行,其他线程排队等待 | course/Demo04 注释 |
|
|
|
|
|
+| **线程安全三大特性** | **原子性**(防读-改-写打断→synchronized/Lock/Atomic)、**可见性**(防 CPU 缓存→volatile/锁)、**有序性**(防指令重排→volatile/synchronized);"原可序"是线程安全核心 | course/Demo04 注释 |
|
|
|
|
|
+| **synchronized 与监视器锁** | Java 内置关键字,保证原子性最简单可靠手段;每个对象底层对应一把监视器锁(Monitor Lock),进 synchronized 前必须拿锁,拿不到进 **BLOCKED** 排队,持有线程执行完自动释放 | course/Demo04 注释 |
|
|
|
|
|
+| **synchronized 优点 / 弊端** | 优点:同一时刻只有一个线程执行被保护代码;弊端:线程多时竞争同一把锁耗资源、降效率 → **锁粒度尽量小**,只锁共享数据代码 | course/Demo04 注释 |
|
|
|
|
|
+| **同步实例方法(写法1)** | `public synchronized void increment()` 锁的是当前对象 **this**,锁住同一对象的方法调用 | course/SynchronizedDemo |
|
|
|
|
|
+| **同步静态方法(写法2)** | `public static synchronized void staticIncrement()` 锁的是**类的 Class 对象**,全局唯一 | course/SynchronizedDemo |
|
|
|
|
|
+| **同步代码块(写法3)** | `synchronized(lock) { ... }` 锁**指定的任意对象**,最灵活、粒度最细、性能更好 | course/SynchronizedDemo |
|
|
|
|
|
+| **转账业务场景** | `synchronized(this)` 把"检查-修改"(balance>=amount 判断 + balance-=amount)整体锁住,防止并发透支 | course/SynchronizedDemo |
|
|
|
|
|
+| **加锁后 count 验证** | 1000 线程调用加锁的 increment03(),count **必然是 1000**(对比 Counter 无锁时小于 1000);方法引用 `sd::increment03` | course/SynchronizedDemo main |
|
|
|
|
|
+| **CooperationTest 练习完成版** | join(主线程等 worker-1)/ yield(每轮让出 CPU)/ interrupt(中断 sleep 长任务,catch 重设标志);打印用 Thread.interrupted()(检查并清除标志) | exericse/CooperationTest |
|
|
|
|
|
+| **随堂练习:三种创建线程方式** | 继承 Thread / 实现 Runnable / 实现 Callable 各写一个线程任务 | exericse/Exercise01 |
|
|
|
|
|
+| **线程常用方法** | 获取当前线程名 getName / 设置线程名 setName / 获取返回结果 get | exericse/Exercise01 |
|
|
|
|
|
+| **实现 Callable 接口** | `implements Callable<Integer>`,泛型指定返回类型;重写 `call()` 带返回值 | homework0808/Homework_03 |
|
|
|
|
|
+| **FutureTask 包装 Callable** | `new FutureTask<>(callable)` 包装任务,传给 `new Thread(futureTask, "线程名")` | homework0808/Homework_03 |
|
|
|
|
|
+| **get() 获取线程返回值** | `ft.get()` 阻塞等待该线程 call() 计算完成,返回结果 | homework0808/Homework_03 |
|
|
|
|
|
+| **多线程独立求和** | 三个 SumCallable 分别求 1~100 / 1~1000 / 1~10000 的和,各自线程并行计算 | homework0808/Homework_03 |
|
|
|
|
|
+| **三种实现方式适用场景** | run() 无返回值(只执行任务);call() 有返回值(需拿回计算结果) | homework0808/Homework_03 注释 |
|
|
|
|
|
+| **作业类名规范化** | `homework_03` 重命名为 `Homework_03`(类名遵循大驼峰规范),逻辑不变 | homework0808/Homework_03 |
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 14. 随堂练习要点
|
|
|
|
|
+
|
|
|
|
|
+- **加锁后 count 验证实验**:运行 `SynchronizedDemo`,观察加锁后 1000 线程 count **必然是 1000**;与第 9 节 `Counter`(无锁 count 往往小于 1000)对比,直观体会 synchronized 解决原子性问题的效果
|
|
|
|
|
+- **三种写法改造实验**:把 `SynchronizedDemo` 的 `increment03`(同步代码块)改成同步实例方法 / 同步静态方法,分别运行验证结果;对比三种写法在"锁的对象"上的差异(this / Class 对象 / 指定对象)
|
|
|
|
|
+- **锁粒度实验**:给 `transfer()` 中的"检查-修改"去掉 synchronized 再运行多次,观察余额是否出现透支(负数);加上 synchronized(this) 后验证正确——体会"检查-修改必须整体锁住"
|
|
|
|
|
+- **卖票问题加锁修复实验**:给 `SellTicket.run()` 中操作 tickets 的代码块加 `synchronized(this)`,再次运行观察相同票和负数票是否消失(回顾第 9 节的预演,本次正式验证)
|
|
|
|
|
+- **BLOCKED 状态观察**:在线程 run() 中进入 synchronized 代码块前后打印 `getState()`,让多个线程竞争同一把锁,观察抢锁失败线程是否处于 **BLOCKED** 状态
|
|
|
|
|
+- **不同锁对象失效实验**:把 `SynchronizedDemo` main 中 `sd::increment03` 改成每次 `new SynchronizedDemo()` 再调用 increment03,观察 count 是否不再等于 1000——体会"锁对象必须一致才有效"
|
|
|
|
|
+- **卖票问题复现实验**:运行 `Demo04`(3 个售票窗口线程),多运行几次,观察**相同的票多次出现**和**负数票**两个 bug——体会数据竞争(每次结果都可能不同、错误无法复现)
|
|
|
|
|
+- **计数器问题复现实验**:运行 `Counter`(1000 个线程同时 count++),多次运行观察最终 count **往往小于 1000**——体会 count++ 的"读-改-写"非原子性
|
|
|
|
|
+- **缩小并发窗口实验**:把 `SellTicket` 的 `Thread.sleep(10)` 去掉或加大到 100,观察卖票 bug 出现的频率变化——sleep 越大(并发窗口越大),bug 越容易复现
|
|
|
|
|
+- **线程安全解决预演**:试着给 `SellTicket.run()` 中操作 tickets 的代码块加 `synchronized(this) { ... }`,再次运行观察相同票和负数票是否消失——提前体验"锁"的作用(后续课程正式学习)
|
|
|
|
|
+- **三个原因分析练习**:对照"多线程安全问题 3 原因",分析卖票问题和计数器问题分别满足哪 3 个条件(缺一不可),加深理解
|
|
|
|
|
+- **三大特性识记**:背下"原子性、可见性、有序性(原可序)"及其对应解决方案(原子性→synchronized/Lock/Atomic;可见性→volatile/锁;有序性→volatile/synchronized)
|
|
|
|
|
+- **CooperationTest 练习**:补全 `exericse/CooperationTest` 中的 TODO,运行验证 join / yield / interrupt 三种方法的协作效果;把打印处 `Thread.interrupted()` 换成 `isInterrupted()`,对比两种方法在"检查后是否清除标志"上的差异
|
|
|
|
|
+- **yield 让出 CPU 实验**:运行 `ThreadMethodDemo`,观察"yield线程"打印"第 i 轮"与 main 等其他线程输出的交错情况;把 `Thread.yield()` 注释掉再运行对比,体会 yield 主动让出 CPU 时间片的效果
|
|
|
|
|
+- **yield vs sleep 对比**:把 `Thread.yield()` 换成 `Thread.sleep(1)` 运行,对比两者对输出交错密度的影响——yield 只是让出一次(可能马上又被调度),sleep 是限时休眠(至少等 1 毫秒)
|
|
|
|
|
+- **isAlive 实验**:在 main 中启动子线程后立刻打印 `t.isAlive()`(true),等子线程 run() 结束后再打印(false);对比 NEW(未 start)与 TERMINATED 状态的 isAlive 返回值
|
|
|
|
|
+- **wait/notify 入门实验**:写一个简单的生产者-消费者雏形——一个线程对对象调用 `wait()` 等待,另一个线程 `sleep` 后对同一对象调用 `notify()` 唤醒,观察等待线程是否被唤醒继续执行(需在 synchronized 同步块中调用,提前体会线程同步)
|
|
|
|
|
+- **wait(timeout) 限时实验**:线程调用 `wait(2000)` 后不调用 notify,观察等待线程是否在 2 秒后**自动恢复**执行——对应第 7 节状态表中 wait(timeout) 进入 TIMED_WAITING
|
|
|
|
|
+- **线程生命周期状态打印实验**:在 `MyThread01` 的 run() 不同位置打印 `Thread.currentThread().getState()`,观察线程在 main 中 `new Thread()` 后(NEW)、`start()` 后(RUNNABLE)、`sleep()` 期间(TIMED_WAITING)、`run()` 结束后(TERMINATED)的状态变化
|
|
|
|
|
+- **join 等待实验**:运行 `Demo03`,把 `mt2.join()` 注释掉再运行一次,对比两次输出中 main 与线程2 打印顺序的差异——有 join 时 main 会**等待线程2 结束**,无 join 时 main 与线程2 并发交错
|
|
|
|
|
+- **join(timeout) 实验**:把 `ThreadMethodDemo` 中 `t.join(1000)` 改成 `t.join()`(无限等),观察 main 是否一直等 worker-2 睡满 5 秒;再改回 `join(1000)`,体会"最多等待指定毫秒,超过就不等"
|
|
|
|
|
+- **interrupt 主动检查实验**:运行 `ThreadMethodDemo`,把 `t1.interrupt()` 注释掉,观察线程 t1 是否**一直循环打印**停不下来;恢复 interrupt 后再观察 t1 检测到中断标志正常退出
|
|
|
|
|
+- **interrupt 阻塞中断实验**:在线程 run() 中先 `Thread.sleep(10000)`,main 里 `sleep(500)` 后 `t1.interrupt()`——观察 sleep 中抛 `InterruptedException`、catch 里重设标志后 `isInterrupted()` 恢复为 true 的过程
|
|
|
|
|
+- **sleep 与 TIMED_WAITING**:在 run() 中 `Thread.sleep(100)` 前后分别打印 `Thread.currentThread().getState()`,验证 sleep 期间线程处于 **TIMED_WAITING**(计时等待)状态
|
|
|
|
|
+- **守护线程设置时机实验**:运行 `Demo02`(代码已把 `setDaemon(true)` 放在 `start()` **之前**,设置合法生效),观察"线程1"(用户线程)持续打印,而"守护线程"在用户线程结束后被 JVM 强制终止;再把 `setDaemon(true)` 故意移回 `mt2.start()` **之后**运行,观察抛 `IllegalThreadStateException`,体会"设置时机"特点
|
|
|
|
|
+- **JVM 退出条件实验**:只启动一个普通子线程(不设守护)跑几秒就退出 main,观察**JVM 是否立即退出**——只要用户线程还在跑,JVM 就"卡着不退";再把该线程设为守护线程,观察 main 结束后 JVM 直接退出
|
|
|
|
|
+- **isDaemon 判断实验**:在线程 run() 中打印 `Thread.currentThread().isDaemon()`,对比普通线程与守护线程的输出差异(true / false)
|
|
|
|
|
+- **守护属性继承实验**:在守护线程的 run() 中再 new 一个子线程并启动,用 `isDaemon()` 打印它的守护属性,验证"守护线程创建的子线程默认也是守护线程"
|
|
|
|
|
+- **finally 不保证实验**:在守护线程 run() 中写 try-finally(finally 里打印"清理资源"),让 JVM 在它执行完前退出,观察 finally 是否被执行——体会"守护线程被强杀时 finally 不一定执行,不能用于资源清理"
|
|
|
|
|
+- **sleep 实验**:把 `Thread.sleep(100)` 的毫秒数改成 1000 / 10,运行 Demo01,观察两条线程输出的**交错密度**变化——休眠越长切换越明显;再分别注释 sleep 前后的 `start()` 对比
|
|
|
|
|
+- **优先级实验**:把 t1 优先级改成 10、t2 保持 5(或改成 1),运行多次,用 `Thread.currentThread().getName()` 统计两个线程输出的条数,观察**高优先级线程获得 CPU 机会是否更多**
|
|
|
|
|
+- **getPriority 验证**:在 `new Thread(run)` 前后分别打印 `getPriority()`,验证新线程默认优先级都是 5;再试试 `setPriority(0)` / `setPriority(11)`,观察抛 `IllegalArgumentException`
|
|
|
|
|
+- **随机性实验**:反复运行 Demo01 多次,观察每次控制台输出顺序是否一致——体会"谁抢到 CPU 时间片不确定"
|
|
|
|
|
+- **run 与 start 对比**:把 `t1.start()` 改成 `t1.run()`,观察是否还有两个线程交替——直接调 run() 不会创建新线程
|
|
|
|
|
+- **三种方式综合练习**:仿照 Exercise01 的要求,分别用继承 Thread、实现 Runnable、实现 Callable 各写一个"打印 0~99 并求和"的线程任务,对比三种代码结构
|
|
|
|
|
+- **常用方法练习**:在 Runnable / Callable 实现类中,用 `Thread.currentThread().getName()` 打印线程名;用 `new Thread(task, "指定名")` 或 `setName("新名")` 设置线程名,验证 getName 的结果
|
|
|
|
|
+- **课后作业运行**:运行 `homework0808/Homework_03`,观察控制台依次打印三个线程各自的求和结果,再通过 `ft1.get()` / `ft2.get()` / `ft3.get()` 拿到返回值,验证三个线程是否并发计算(结果正确、输出顺序不定)
|
|
|
|
|
+- **get() 汇总思考**:作业要求"在 main 中汇总总结果"——把 `ft1.get() + ft2.get() + ft3.get()` 相加,打印三种规模求和的**总和**,体会"主线程等待子线程结果并汇总"
|
|
|
|
|
+- **三种方式适用场景对比**:思考题——`run()` 无返回值适合"烧水、打印、发消息"这类只执行不看结果的场景;`call()` 有返回值适合"求和、查询数据库、计算成绩"这类**需要拿结果继续处理**的场景
|
|
|
|
|
+- **知识点串联**:把 20260808 的"三种实现方式 + 线程调度"与今天的"sleep / 优先级"串成一条线——**创建线程(3 种方式)→ 控制线程(sleep / 优先级)→ 获取结果(get)**
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 15. 拓展阅读
|
|
|
|
|
+
|
|
|
|
|
+- **JDK 文档**:[`java.lang.Thread`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/Thread.html)(线程类,`sleep()` / `getPriority()` / `setPriority()` / `getName()` / `setName()` / `currentThread()` / `setDaemon()` / `isDaemon()` / `join()` / `interrupt()` / `isInterrupted()` / `getState()` / `yield()` / `isAlive()` 等核心方法)、[`java.lang.Object`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/Object.html)(`wait()` / `notify()` / `notifyAll()` 线程通信方法)、[`java.lang.Thread.State`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/Thread.State.html)(线程 6 种状态枚举)
|
|
|
|
|
+- **线程安全入门**:多线程的**数据竞争**是并发编程的根源性问题——卖票问题(相同票 / 负数票)和计数器问题(count 小于理论值)是理解线程安全最经典的入门案例,都源于**读-改-写复合操作被并发打断**
|
|
|
|
|
+- **原子操作**:`count++` 不是原子操作(读-改-写 3 步);Java 提供 `AtomicInteger` / `AtomicLong` 等**原子类**,其内部方法(如 `incrementAndGet()`)是原子的,无需加锁即可保证线程安全
|
|
|
|
|
+- **volatile 关键字**:`volatile` 修饰的变量**不缓存到 CPU 寄存器 / 缓存**,保证**可见性**(一个线程的修改对其他线程立即可见);但它**不能保证原子性**(count++ 用 volatile 依然不安全),需配合锁或原子类
|
|
|
|
|
+- **synchronized 预读**:卖票问题的标准解法是 `synchronized` 同步块 / 同步方法——同一时刻只允许一个线程进入临界区,其余线程**排队等待**(对应线程状态中的 **BLOCKED** 状态,抢锁失败被动阻塞)
|
|
|
|
|
+- **synchronized 的锁对象选择**:锁必须选**所有线程共享的对象**才有效——同步实例方法锁 this、同步静态方法锁 Class 对象、同步代码块锁指定对象;如果各线程 new 各自对象去加锁(锁对象不一致),synchronized 就**失效**了
|
|
|
|
|
+- **synchronized 的可重入性**:`synchronized` 是**可重入锁**——同一线程可以重复获取自己已经持有的锁(如同步方法内再调用另一个同步方法),不会自己把自己锁死;每个监视器锁都关联一个持有计数
|
|
|
|
|
+- **synchronized 与 wait/notify**:`wait()` / `notify()` 必须**持有 synchronized 锁**时调用(回顾第 8 节 Object 类方法)——它们本质是"释放锁并等待" / "唤醒等待并重新竞争锁",是 synchronized 锁之上实现的线程通信
|
|
|
|
|
+- **指令重排**:编译器和 CPU 为了优化性能可能**重排指令执行顺序**(有序性问题);`volatile` 通过内存屏障禁止重排,`synchronized` 通过锁保证临界区内代码的有序执行
|
|
|
|
|
+- **Lock 与 synchronized 对比**:JDK5 引入的 `Lock` 接口(如 `ReentrantLock`)功能更强——可尝试 `tryLock()`(尝试获取不阻塞)、`lockInterruptibly()`(可中断获取锁)、公平锁;但**使用更复杂**(需手动 unlock),synchronized 是内置关键字更简单
|
|
|
|
|
+- **debug 多线程 bug 的困难**:数据竞争类 bug **错误无法复现**(每次运行结果可能不同),排查时常用思路是**缩小并发窗口**(加大 sleep 放大 bug)、**线程转储(thread dump)**、**添加日志打印中间状态**
|
|
|
|
|
+- **生产环境线程安全**:真实项目中很少手写 synchronized 管理大量线程,而是用**线程池(ExecutorService)** + **并发容器(ConcurrentHashMap / CopyOnWriteArrayList)** + **原子类**等 JDK 提供的线程安全组件
|
|
|
|
|
+- **守护线程与 JVM 生命周期**:JVM 在**最后一个非守护线程结束**时才退出(守护线程不阻止退出);`main` 方法本身运行在用户线程上,但 `main` 返回**不代表** JVM 退出——只要还有用户线程存活,JVM 就继续运行(这正是很多服务程序"关不掉"的原因)
|
|
|
|
|
+- **setDaemon 的时机限制**:`setDaemon(true)` 必须在 `start()` **之前**调用,否则抛 `IllegalThreadStateException`;线程启动后守护属性不可再修改
|
|
|
|
|
+- **守护线程的典型应用**:GC 垃圾回收线程是最经典的守护线程;此外**心跳检测、后台日志、监控统计、自动保存**等"随主程序生灭"的后台任务都适合用守护线程,避免程序退出时被"后台线程卡住"
|
|
|
|
|
+- **守护线程 vs 用户线程选型**:需要**程序结束前必须完成**的任务(如写文件、释放连接)用**用户线程**;"可有可无、主程序结束就应终止"的任务用**守护线程**——因为守护线程被强杀时 **finally 不保证执行**,不能依赖它做资源清理
|
|
|
|
|
+- **Thread 优先级常量**:Thread 类内置三个优先级常量——`MIN_PRIORITY = 1`、`NORM_PRIORITY = 5`(默认)、`MAX_PRIORITY = 10`;`setPriority()` 参数超出 1~10 范围会抛 `IllegalArgumentException`
|
|
|
|
|
+- **sleep 与 yield 的区别**:`Thread.sleep(ms)` 让线程休眠指定毫秒(可被中断,抛 InterruptedException);`Thread.yield()` 只**让出一次 CPU**(回到就绪状态,马上又可能被调度)——都是主动让出 CPU 的手段
|
|
|
|
|
+- **wait 与 sleep 的区别(再深化)**:`wait()` 是 Object 方法,**必须持有对象锁**调用,调用后**释放锁**并进入 WAITING,需 `notify()` / `notifyAll()` 唤醒;`sleep()` 是 Thread 静态方法,**不释放锁**,时间到自动醒——两者是"线程通信"与"线程暂停"的分水岭
|
|
|
|
|
+- **notify vs notifyAll**:`notify()` 只唤醒**一个**等待该对象的线程(具体哪个由 JVM 决定);`notifyAll()` 唤醒**所有**等待线程——多个线程等待同一条件时优先用 notifyAll,避免"通知丢失"导致部分线程永久等待
|
|
|
|
|
+- **wait/notify 的应用**:经典的**生产者-消费者模式**——生产者生产数据后 `notify()`,消费者 `wait()` 等待数据;缓冲区满时生产者等待、缓冲区空时消费者等待,实现线程间的协调与解耦
|
|
|
|
|
+- **yield 的适用场景**:`yield()` 适合"高优先级线程主动谦让、避免饿死低优先级线程"等场景;但它是**建议性**的,生产环境一般不依赖它做精确控制(调度器可能忽略)
|
|
|
|
|
+- **isAlive 的典型用途**:`isAlive()` 常用于轮询判断子线程是否执行完(`while (t.isAlive()) { ... }`);但更优雅的做法是用 `join()` 阻塞等待或 `FutureTask.get()` 获取结果(避免空转消耗 CPU)
|
|
|
|
|
+- **sleep 与 wait 的区别**:`sleep()` 是 Thread 的静态方法,**不释放锁**(monitor);`wait()` 是 Object 的实例方法,必须持有锁时调用且**会释放锁**(后续线程同步课程会深入)
|
|
|
|
|
+- **线程状态(Thread.State 枚举)**:Java 定义 6 种状态——NEW / RUNNABLE(就绪+运行合并)/ BLOCKED(抢锁失败)/ WAITING(无限等)/ TIMED_WAITING(限时等)/ TERMINATED(终点);`sleep()` 期间线程处于 **TIMED_WAITING(计时等待)** 状态;`join()` 让调用线程进入 **WAITING**,`join(timeout)` 进入 **TIMED_WAITING**
|
|
|
|
|
+- **getState() 方法**:`Thread.getState()` 返回线程当前状态对应的 `Thread.State` 枚举值,配合演示可以观察线程从 NEW → RUNNABLE → TIMED_WAITING → TERMINATED 的完整生命周期
|
|
|
|
|
+- **join 的应用场景**:`join()` 常用于"主线程等待子线程计算结果/执行完成后,再继续处理"的场景;`join(timeout)` 适合"最多等多久"的限时协作(如限时等待子任务完成)
|
|
|
|
|
+- **interrupt 的正确用法**:`interrupt()` 是 Java 协作式中断的标准手段——线程内部**主动检查中断标志**(`isInterrupted()` / `Thread.interrupted()`)或在阻塞方法中收到 `InterruptedException` 来响应;`InterruptedException` 抛出时会**清除中断标志**,catch 后重设标志是常见最佳实践
|
|
|
|
|
+- **Thread.interrupted() vs isInterrupted()**:静态方法 `Thread.interrupted()` 检查中断标志**并清除**;实例方法 `isInterrupted()` 只检查**不清除**——两者都常用于循环任务的中断响应
|
|
|
|
|
+- **FutureTask.get() 与 join 的关联**:第 8 节作业中 `ft.get()` 获取 Callable 结果,本质也是**阻塞等待**线程任务完成——与 `join()` 有相似的"等待"语义(get() 底层也是等待任务结束)
|
|
|
|
|
+- **抢占式调度的局限**:设置优先级只是"提高获得 CPU 的概率",**不能保证执行顺序**,也不适合做严格的任务先后控制——生产环境更常用**线程池(ExecutorService)** + 任务队列来精细控制
|
|
|
|
|
+- **FutureTask 与 Future 接口**:`FutureTask` 实现了 `Future` 接口——`get()` 获取结果、`isDone()` 判断完成、`cancel()` 取消任务;`get()` 是**阻塞方法**,会一直等待该线程任务执行完才返回
|
|
|
|
|
+- **Callable 的现代写法**:生产环境中更常用 `ExecutorService.submit(Callable)` 直接返回 `Future`,比手动 `new Thread + FutureTask` 更规范(线程复用、统一管理)
|
|
|
|
|
+- **Lambda 回顾**:Runnable 是**函数式接口**(只有一个抽象方法 run()),所以可以用 Lambda 表达式创建任务对象(回顾 08-04 函数式编程);Demo01 中 `Runnable run = () -> {...}` 就是典型写法
|
|
|
|
|
+- **线程安全预告**:多线程并发访问**共享数据**时会引发数据竞争(如两个线程同时给同一个变量自增);后续将学习 `synchronized` 同步、`Lock` 锁、`volatile` 等机制解决线程安全问题
|
|
|
|
|
+- **任务与线程解耦**:Runnable / Callable 描述"**任务**"(做什么),Thread / FutureTask 承载"**线程**"(怎么跑)——同一个任务对象可以交给多个线程执行,这种设计更符合面向接口编程思想
|