# 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` + 重写 `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 { 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` 泛型** = `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 包装每个 Callable 对象 FutureTask ft1 = new FutureTask<>(sc1); FutureTask ft2 = new FutureTask<>(sc2); FutureTask 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(sc1) → new Thread(ft1, "线程1") → t1.start() ↓ 异步执行 call() ft1.get() ←————— 等待线程1计算完成,返回 call() 的返回值(5050) ``` > **要点**: > - **FutureTask 是"中间人"**:Callable 不能直接交给 Thread,用 `FutureTask` 包装后传给 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`,泛型指定返回类型;重写 `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 承载"**线程**"(怎么跑)——同一个任务对象可以交给多个线程执行,这种设计更符合面向接口编程思想