# muti-threading **Repository Path**: woldier/muti-threading ## Basic Information - **Project Name**: muti-threading - **Description**: 以知识点为主线、穿插讲解"应用","原理"和"多线程设计模式",多维度学懂并发 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2022-10-19 - **Last Updated**: 2023-06-16 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # java 多线程 ## 1. 准备工作 本项目是基于jdk8及以上的 ```xml org.projectlombok lombok 1.18.10 ch.qos.logback logback-classic 1.2.3 ``` `logback.xml` 配置 ```xml %date{HH:mm:ss} [%t] %logger - %m%n ``` ## 2. 进程与线程 ### 2.1 进程与线程的概念 - 进程 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在 指令运行过程中还需要用到磁盘、网络等设备 进程就是用来加载指令、管理内存、管理 IO 的 当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。 进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器 等),也有的程序只能启动一个实例进程(例如网易云音乐、360 安全卫士等) - 线程 一个进程之内可以分为一到多个线程。 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行 Java 中,线程作为最小调度单位,进程作为资源分配的最小单位。 在 windows 中进程是不活动的,只是作 为线程的容器 - 两者的对比 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集 进程拥有共享的资源,如内存空间等,供其内部的线程共享 进程间通信较为复杂 同一台计算机的进程通信称为 IPC(Inter-process communication) 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低 ### 2.2 并行与并发 ![image-20221019172021660](https://woldier-pic-repo-1309997478.cos.ap-chengdu.myqcloud.com/image-20221019172021660.png) 单核 cpu 下,线程实际还是 `串行执行` 的。操作系统中有一个组件叫做任务调度器,将 cpu 的时间片(windows 下时间片最小约为 15 毫秒)分给不同的程序使用,只是由于 cpu 在线程间(时间片很短)的切换非常快,人类感 觉是 `同时运行`的 。总结为一句话就是: 微观串行,宏观并行 , 一般会将这种 线程轮流使用 CPU 的做法称为并发, concurrent(并发) ![image-20221019171941223](https://woldier-pic-repo-1309997478.cos.ap-chengdu.myqcloud.com/image-20221019171941223.png) 多核 cpu下,每个 核(core) 都可以调度运行线程,这时候线程可以是并行的。 ![image-20221019172044316](https://woldier-pic-repo-1309997478.cos.ap-chengdu.myqcloud.com/image-20221019172044316.png) ![image-20221019172000011](https://woldier-pic-repo-1309997478.cos.ap-chengdu.myqcloud.com/image-20221019172000011.png) 引用 Rob Pike 的一段描述: 并发(concurrent)是同一时间应对(dealing with)多件事情的能力 并行(parallel)是同一时间动手做(doing)多件事情的能力 例子 家庭主妇做饭、打扫卫生、给孩子喂奶,她一个人轮流交替做这多件事,这时就是并发 家庭主妇雇了个保姆,她们一起这些事,这时既有并发,也有并行(这时会产生竞争,例如锅只有一口,一 个人用锅时,另一个人就得等待) ### 2.3 应用 以调用方角度来讲,如果 需要等待结果返回,才能继续运行就是同步 不需要等待结果返回,就能继续运行就是异步 1. 设计 多线程可以让方法执行变为异步的(即不要巴巴干等着)比如说读取磁盘文件时,假设读取操作花费了 5 秒钟,如 果没有线程调度机制,这 5 秒 cpu 什么都做不了,其它代码都得暂停... 2. 结论 比如在项目中,视频文件需要转换格式等操作比较费时,这时开一个新线程处理视频转换,避免阻塞主线程 tomcat 的异步 servlet 也是类似的目的,让用户线程处理耗时较长的操作,避免阻塞 tomcat 的工作线程 ui 程序中,开线程进行其他操作,避免阻塞 ui 线程 --- 充分利用多核 cpu 的优势,提高运行效率。想象下面的场景,执行 3 个计算,最后将计算结果汇总。 ![image-20221019172739723](https://woldier-pic-repo-1309997478.cos.ap-chengdu.myqcloud.com/image-20221019172739723.png) - 如果是串行执行,那么总共花费的时间是 10 + 11 + 9 + 1 = 31m - 但如果是四核 cpu,各个核心分别使用线程 1 执行计算 1,线程 2 执行计算 2,线程 3 执行计算 3,那么 3 个 线程是并行的,花费时间只取决于最长的那个线程运行的时间,即 11ms 最后加上汇总时间只会花费 12ms > 注意 :需要在多核 cpu 才能提高效率,单核仍然时是轮流执行 1. 设计 见<并发编程_应用.pdf> .1 2. 结论 1.单核 cpu 下,多线程不能实际提高程序运行效率,只是为了能够在不同的任务之间切换,不同线程轮流使用 cpu ,不至于一个线程总占用 cpu,别的线程没法干活 2.多核 cpu 可以并行跑多个线程,但能否提高程序运行效率还是要分情况的 - 有些任务,经过精心设计,将任务拆分,并行执行,当然可以提高程序的运行效率。但不是所有计算任 务都能拆分 - 也不是所有任务都需要拆分,任务的目的如果不同,谈拆分和效率没啥意义 3. IO 操作不占用 cpu,只是我们一般拷贝文件使用的是【阻塞 IO】,这时相当于线程虽然不用 cpu,但需要一 直等待 IO 结束,没能充分利用线程。所以才有后面的【非阻塞 IO】和【异步 IO】优化 ## 3. java线程 ### 3.1 创建和运行线程 - 方法一,直接使用Thread ```java Thread t = new Thread(){ public void run(){ //要执行的任务 } }; //启动线程 t.start(); ``` eg:(com.ch3.ThreadDemo_01) ```java @Slf4j(topic = "c.Thread") public class ThreadDemo_01 { public static void main(String[] args) { new Thread(){ @Override public void run() { log.info("debug............."); } }.start(); log.info("debug............."); } } ``` ![image-20221019184943067](https://woldier-pic-repo-1309997478.cos.ap-chengdu.myqcloud.com/image-20221019184943067.png) --- - 方法二,使用 Runnable 配合 Thread ```java unnable runnable = new Runnable() { public void run(){ // 要执行的任务 } }; // 创建线程对象 Thread t = new Thread( runnable ); // 启动线程 t.start(); ``` eg(com.ch3.ThreadDemo_02): ```java @Slf4j(topic = "c.Thread") public class ThreadDemo_02 { public static void main(String[] args) { Runnable runnable = new Runnable() { @Override public void run() { log.info("debug............."); } }; new Thread(runnable).start(); log.info("debug............."); } } ``` ![image-20221019185929681](https://woldier-pic-repo-1309997478.cos.ap-chengdu.myqcloud.com/image-20221019185929681.png) Java 8 以后可以使用 lambda 精简代码 ```java // 创建任务对象 Runnable task2 = () -> log.debug("hello"); // 参数1 是任务对象; 参数2 是线程名字,推荐 Thread t2 = new Thread(task2, "t2"); t2.start(); ``` eg(ThreadDemo_03): ```java @Slf4j(topic = "c.Thread") public class ThreadDemo_03 { public static void main(String[] args) { Runnable r = () ->{ log.info("debug..................."); }; new Thread(r,"T-runnable").start(); log.info("debug..................."); } } ``` ![image-20221019190238256](https://woldier-pic-repo-1309997478.cos.ap-chengdu.myqcloud.com/image-20221019190238256.png) - 原理之 Thread 与 Runnable 的关系 分析 Thread 的源码,理清它与 Runnable 的关系 通过构造方法中调用int()重载方法,将Runnable对象设置给自身的成员变量,然后在调用Thread.start()方法时通过如下逻辑进行调用: ![image-20221019200028784](https://woldier-pic-repo-1309997478.cos.ap-chengdu.myqcloud.com/image-20221019200028784.png) 小结: 方法1 是把线程和任务合并在了一起,方法2 是把线程和任务分开了 用 Runnable 更容易与线程池等高级 API 配合 用 Runnable 让任务类脱离了 Thread 继承体系,更灵活 - 方法三,FutureTask 配合 Thread FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况 ```java // 创建任务对象 FutureTask task3 = new FutureTask<>(() -> { log.debug("hello"); return 100; }); // 参数1 是任务对象; 参数2 是线程名字,推荐 new Thread(task3, "t3").start(); // 主线程阻塞,同步等待 task 执行完毕的结果 Integer result = task3.get(); log.debug("结果是:{}", result); ``` ![image-20221019202447031](https://woldier-pic-repo-1309997478.cos.ap-chengdu.myqcloud.com/image-20221019202447031.png) ### 3.2 观察多个线程同时运行 主要是理解 - 交替执行 - 谁先谁后,不由我们控制 ```java package com.ch3; import lombok.extern.slf4j.Slf4j; @Slf4j(topic = "c.Thread") public class ThreadDemo_05 { public static void main(String[] args) { new Thread(()->{while(true) log.info("{} is running...........",Thread.currentThread().getName());},"th-1").start(); new Thread(()->{while(true) log.info("{} is running...........",Thread.currentThread().getName());},"th-2").start(); } } ``` ### 3.3 查看进程线程的方法 windows: - 任务管理器可以查看进程和线程数,也可以用来杀死进程 - `tasklist | findstr `,`jps`查看进程 - `taskkill/F /PID 进程号` 杀死进程 linux: - ps -fe 查看所有进程 - ps -fT -p 查看某个进程(PID)的所有线程 - kill 杀死进程 - top 按大写 H 切换是否显示线程 - top -H -p 查看某个进程(PID)的所有线程 java: - jps 命令查看所有 Java 进程 - jstack 查看某个 Java 进程(PID)的所有线程状态 - jconsole 来查看某个 Java 进程中线程的运行情况(图形界面) jconsole 远程监控配置 1. 需要以如下方式运行你的 java 类 ```shell java -Djava.rmi.server.hostname= -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=<连接端口 ture|false> -Dcom.sun.management.jmxremote.ssl=<是否安全连接 ture|false> -Dcom.sun.management.jmxremote.authenticate=是否认证 ``` 2. 修改 /etc/hosts 文件将 127.0.0.1 映射至主机名 3. 如果要认证访问,还需要做如下步骤 复制 jmxremote.password 文件,修改 jmxremote.password 和 jmxremote.access 文件的权限为 600 即文件所有者可读写,连接时填入 controlRole(用户名),R&D(密码) ### 3.4 原理之线程运行 - 栈与栈帧 Java Virtual Machine Stacks (Java 虚拟机栈) 我们都知道 JVM 中由堆、栈、方法区所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟 机就会为其分配一块栈内存。 1. 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存 2. 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法 - 线程上下文切换(Thread Context Switch) 因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码 1. 线程的 cpu 时间片用完 2. 垃圾回收 3. 有更高优先级的线程需要运行 4. 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法 当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念 就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的 1. 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等 2. Context Switch 频繁发生会影响性能 ### 3.5 常见方法 | 方法名 | static | 功能说明 | 注意 | | ---------------- | ------ | ------------------------------------------------------------ | :----------------------------------------------------------- | | start() | | 启动一个新线 程,在新的线程 运行 run 方法 中的代码 | start 方法只是让线程进入就绪,里面代码不一定立刻 运行(CPU 的时间片还没分给它)。每个线程对象的 start方法只能调用一次,如果调用了多次会出现 IllegalThreadStateException | | run() | | 新线程启动后会 调用的方法 | 如果在构造 Thread 对象时传递了 Runnable 参数,则 线程启动后会调用 Runnable 中的 run 方法,否则默 认不执行任何操作。但可以创建 Thread 的子类对象, 来覆盖默认行为 | | join() | | 等待线程运行结 束 | | | join(long n) | | 等待线程运行结 束,最多等待 n 毫秒 | | | getId() | | 获取线程长整型 的 id | id 唯一 | | getName() | | 获取线程名 | | | setName(String) | | 修改线程名 | | | getPriority() | | 获取线程优先级 | | | setPriority(int) | | 修改线程优先级 | java中规定线程优先级是1~10 的整数,较大的优先级 能提高该线程被 CPU 调度的机率 | | isInterrupted() | | 判断是否被打 断, | 不会清除 打断标记 | | isAlive() | | 线程是否存活 (还没有运行完 毕) | | | interrupt() | | 打断线程 | 如果被打断线程正在 sleep,wait,join 会导致被打断 的线程抛出 InterruptedException,并清除 打断标 记 ;如果打断的正在运行的线程,则会设置 打断标 记 ;park 的线程被打断,也会设置 打断标记 | | interrupted() | static | 判断当前线程是 否被打断 | 会清除 打断标记 | | currentThread() | static | 获取当前正在执 行的线程 | | | sleep(long n) | static | 让当前执行的线 程休眠n毫秒, 休眠时让出 cpu 的时间片给其它 线程 | | | yield() | static | 提示线程调度器 让出当前线程对 CPU的使用 | 主要是为了测试和调试 | ### 3.6 start 与 run 调用run ```java public static void main(String[] args) { Thread t1 = new Thread("t1") { @Override public void run() { log.debug(Thread.currentThread().getName()); FileReader.read(Constants.MP4_FULL_PATH); } }; t1.run(); log.debug("do other things ..."); } ``` 输出 ```tiki wiki 19:39:14 [main] c.TestStart - main 19:39:14 [main] c.FileReader - read [1.mp4] start ... 19:39:18 [main] c.FileReader - read [1.mp4] end ... cost: 4227 ms 19:39:18 [main] c.TestStart - do other things ... ``` 程序仍在 main 线程运行, FileReader.read() 方法调用还是同步的 调用 start 将上述代码的 t1.run() 改为 ```java t1.start(); ``` 输出 ```tiki wiki 19:41:30 [main] c.TestStart - do other things ... 19:41:30 [t1] c.TestStart - t1 19:41:30 [t1] c.FileReader - read [1.mp4] start ... 19:41:35 [t1] c.FileReader - read [1.mp4] end ... cost: 4542 ms ``` 程序在 t1 线程运行, FileReader.read() 方法调用是异步的 小结 - 直接调用 run 是在主线程中执行了 run,没有启动新的线程 - 使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码 ### 3.7 sleep 与 yield - sleep 1. 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞) 2. 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException 3. 睡眠结束后的线程未必会立刻得到执行 4. 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性 - yield 1. 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程 2. 具体的实现依赖于操作系统的任务调度器 --- *线程优先级* 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用 ```java Runnable task1 = () -> { int count = 0; for (;;) { System.out.println("---->1 " + count++); } }; Runnable task2 = () -> { int count = 0; for (;;) { // Thread.yield(); System.out.println(" ---->2 " + count++); } }; Thread t1 = new Thread(task1, "t1"); Thread t2 = new Thread(task2, "t2"); // t1.setPriority(Thread.MIN_PRIORITY); // t2.setPriority(Thread.MAX_PRIORITY); t1.start(); t2.start(); ``` 应用之效率(j见pdf案例2) ### 3.8 join 方法详解 - 为什么需要 join 下面的代码执行,打印 r 是什么? ```java @Slf4j(topic = "c.Thread") public class ThreadDemo_07 { static int r = 0; public static void main(String[] args) throws InterruptedException { test1(); } private static void test1() throws InterruptedException { log.debug("开始"); Thread t1 = new Thread(() -> { log.debug("开始"); try { sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } log.debug("结束"); r = 10; }); t1.start(); log.debug("结果为:{}", r); log.debug("结束"); } } ``` 分析 - 因为主线程和线程 t1 是并行执行的,t1 线程需要 1 秒之后才能算出 r=10 - 而主线程一开始就要打印 r 的结果,所以只能打印出 r=0 解决方法 用 sleep 行不行?为什么? 用 join,加在 t1.start() 之后即可 ```java @Slf4j(topic = "c.Thread") public class ThreadDemo_07 { static int r = 0; public static void main(String[] args) throws InterruptedException { test1(); } private static void test1() throws InterruptedException { log.debug("开始"); Thread t1 = new Thread(() -> { log.debug("开始"); try { sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } log.debug("结束"); r = 10; }); t1.start(); t1.join(); log.debug("结果为:{}", r); log.debug("结束"); } } ``` ### 3.9 interrupt 方法详解 - **打断 sleep,wait,join(jion的底层就是wait) 的线程** (即阻塞的线程) 这几个方法都会让线程进入阻塞状态 打断 sleep 的线程, 会`清空打断状态`,以 sleep 为例 ```java /** *interrupt */ @Slf4j(topic = "c.Thread") public class ThreadDemo_08 { public static void main(String[] args) throws InterruptedException { t1(); } public static void t1() throws InterruptedException { Thread thread = new Thread(() -> { try { TimeUnit.SECONDS.sleep(2); log.debug("do sth after sleep"); } catch (InterruptedException e) { log.debug("sleep interrupted"); e.printStackTrace(); } }, "t1"); thread.start(); TimeUnit.SECONDS.sleep(1); log.debug("before interrupt........... ,isInterrupted:{}",thread.isInterrupted()); thread.interrupt(); TimeUnit.SECONDS.sleep(1); log.debug("after interrupt........... ,isInterrupted:{}",thread.isInterrupted()); log.debug("after interrupt........... ,isInterrupted:{}",thread.isInterrupted()); } } ``` ```tiki wiki 16:01:49 [main] c.Thread - before interrupt........... ,isInterrupted:false 16:01:49 [t1] c.Thread - sleep interrupted java.lang.InterruptedException: sleep interrupted at java.lang.Thread.sleep(Native Method) at java.lang.Thread.sleep(Thread.java:340) at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386) at com.ch3.ThreadDemo_08.lambda$t1$0(ThreadDemo_08.java:21) at java.lang.Thread.run(Thread.java:748) 16:01:50 [main] c.Thread - after interrupt........... ,isInterrupted:false 16:01:50 [main] c.Thread - after interrupt........... ,isInterrupted:false ``` - 打断正在运行的线程 ```java /** * interrupt 打断正在运行的线程 */ @Slf4j(topic = "c.Thread") public class ThreadDemo_09 { public static void main(String[] args) throws InterruptedException { t1(); } public static void t1() throws InterruptedException { Thread thread = new Thread(() -> { while (true) { if (Thread.currentThread().isInterrupted()){ log.debug("被打断了"); return;} } }); log.debug("线程开始运行"); thread.start(); TimeUnit.SECONDS.sleep(5); log.debug("打断线程"); thread.interrupt(); } } ``` 运行结果如图 ```shell 16:18:24 [main] c.Thread - 线程开始运行 16:18:29 [main] c.Thread - 打断线程 16:18:29 [Thread-0] c.Thread - 被打断了 Process finished with exit code 0 ``` 正在运行的线程被打断后interrupt标记未true 不会项sleep等被清除,因此我们可以通过逻辑判断来做一些线程推出的善后工作 > # 两阶段终止模式 > > Two Phase Termination 在一个线程 T1 中如何“优雅”终止线程 T2?这里的【优雅】指的是给 T2 一个料理后事的机会 > > ## 1. 错误思路 > > - 使用线程对象的stop()方法停止线程 > - stop 方法会真正杀死线程,如果这时线程锁住了共享资源,那么当它被杀死后就再也没有机会释放锁, 其它线程将永远无法获取锁 > - 使用System.exit(int)方法停止线程 > - 目的仅是停止一个线程,但这种做法会让整个程序都停止 > > ## 2.两阶段终止 > > ![image-20230418163058511](https://woldier-pic-repo-1309997478.cos.ap-chengdu.myqcloud.com/woldier/2023/04/2669d82bd6394b953b16f002fe478e30.png) > > ### 2.1 利用interrupted() > > interrupt 可以打断正在执行的线程,无论这个线程是在 sleep,wait,还是正常运行 > > ```java > @Slf4j(topic = "c.Thread") > public class TPThread { > private Thread thread; > > public void start(Thread t) { > this.thread = t; > this.thread.start(); > } > > public void start() { > Thread t = new Thread(() -> { > while(true){ > /*判断是否终止*/ > if(Thread.currentThread().isInterrupted()){ > log.debug("终止善后工作....."); > break; > } > try { > TimeUnit.SECONDS.sleep(1); > log.debug("阻塞调用.........."); > } catch (InterruptedException e) { > Thread.currentThread().interrupt(); > } > > } > }); > this.start(t); > } > > public void stop(){ > thread.interrupt(); > } > > public static void main(String[] args) throws InterruptedException { > TPThread tpThread = new TPThread(); > log.debug("开始 执行自定义线程"); > tpThread.start(); > > TimeUnit.SECONDS.sleep(10); > log.debug("主动打断"); > tpThread.stop(); > > } > } > > ``` > > ```shell > 16:46:52 [main] c.Thread - 开始 执行自定义线程 > 16:46:53 [Thread-0] c.Thread - 阻塞调用.......... > 16:46:54 [Thread-0] c.Thread - 阻塞调用.......... > 16:46:55 [Thread-0] c.Thread - 阻塞调用.......... > 16:46:56 [Thread-0] c.Thread - 阻塞调用.......... > 16:46:57 [Thread-0] c.Thread - 阻塞调用.......... > 16:46:58 [Thread-0] c.Thread - 阻塞调用.......... > 16:46:59 [Thread-0] c.Thread - 阻塞调用.......... > 16:47:00 [Thread-0] c.Thread - 阻塞调用.......... > 16:47:01 [Thread-0] c.Thread - 阻塞调用.......... > 16:47:02 [main] c.Thread - 主动打断 > 16:47:02 [Thread-0] c.Thread - 终止善后工作..... > > Process finished with exit code 0 > ``` > > - 打断 park 线程 打断 park 线程, 不会清空打断状态 ```java @Slf4j(topic = "c.Thread") public class ThreadDemo_10 { public static void main(String[] args) throws InterruptedException { //t1(); t2();; } public static void t1() throws InterruptedException { Thread thread = new Thread(() -> { log.debug("准备pack"); LockSupport.park(); log.debug("继续执行1"); log.debug("查看打断标记, {}", Thread.currentThread().isInterrupted()); log.debug("尝试再次pack"); LockSupport.park(); log.debug("继续执行2"); }, "pack打断标记"); thread.start(); TimeUnit.SECONDS.sleep(2); thread.interrupt(); } public static void t2() throws InterruptedException { Thread thread = new Thread(() -> { log.debug("准备pack"); LockSupport.park(); log.debug("继续执行"); log.debug("查看打断标记并清空, {}", Thread.interrupted()); log.debug("尝试再次pack"); LockSupport.park(); log.debug("继续执行2"); }, "pack打断标记"); thread.start(); TimeUnit.SECONDS.sleep(1); thread.interrupt(); } } ``` 当执行t1时,虽然进行了两次pack 但是第二次准备pack时打断标记未true 因此不会进入pack 执行结果如下 ```shell 17:09:10 [pack打断标记] c.Thread - 准备pack 17:09:12 [pack打断标记] c.Thread - 继续执行1 17:09:12 [pack打断标记] c.Thread - 查看打断标记, true 17:09:12 [pack打断标记] c.Thread - 尝试再次pack 17:09:12 [pack打断标记] c.Thread - 继续执行2 Process finished with exit code 0 ``` 如果把`Thread.currentThread().isInterrupted()`换成`Thread.currentThread().interrupted()`那么获取打断标记后打断标记被清除了因此可以继续pack ,线程也卡顿在第二个pack处 ```sehll 17:07:08 [pack打断标记] c.Thread - 准备pack 17:07:09 [pack打断标记] c.Thread - 继续执行 17:07:09 [pack打断标记] c.Thread - 查看打断标记并清空, true 17:07:09 [pack打断标记] c.Thread - 尝试再次pack ..........线程pack中 ``` ### 3.10 不推荐使用的方法 还有一些不推荐使用的方法,这些方法已过时,容易破坏同步代码块,造成线程死锁 | 方法名 | 功能说明 | | --------- | ------------- | | stop() | 停止线程 | | suspend() | 挂起,暂停线程 | | resume() | 恢复线程运行 | ### 3.11 守护线程 默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守 护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。 ```java @Slf4j(topic = "c.Thread") public class ThreadDemo_11 { public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(() -> { while(true){ if (Thread.currentThread().isInterrupted()) break; } log.debug("结束了"); },"守护线程"); thread.setDaemon(true); //设置未守护线程 log.debug("开启守护线程"); thread.start(); TimeUnit.SECONDS.sleep(5); log.debug("主线程推出"); } } ``` ```shell 17:24:10 [main] c.Thread - 开启守护线程 17:24:15 [main] c.Thread - 主线程推出 Process finished with exit code 0 ``` 经过观察可以发现主进程退出后 守护进程立即退出了; > 注意 > > - 垃圾回收器线程就是一种守护线程 > - Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等 待它们处理完当前请求 ### 3.12 五种状态 这是从操作系统层面描述的 ![image-20230418172647580](https://woldier-pic-repo-1309997478.cos.ap-chengdu.myqcloud.com/woldier/2023/04/e99dda65e24e3c808de9dd00e43f82e8.png) - 【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联 - 【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行 - 【运行状态】指获取了 CPU 时间片运行中的状态 当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换 - 【阻塞状态】 - 如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入 【阻塞状态】 - 等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】 - 与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑 调度它们 - 【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态 ### 3.13 六种状态 这是从 Java API 层面来描述的 根据 Thread.State 枚举,分为六种状态 ![image-20230418172912122](https://woldier-pic-repo-1309997478.cos.ap-chengdu.myqcloud.com/woldier/2023/04/9f8ad6b457c037ba72b78eff785855cd.png) - `NEW `线程刚被创建,但是还没有调用` start() `方法 - `RUNNABLE `当调用了` start() `方法之后,注意,Java API 层面的 `RUNNABLE `状态涵盖了 操作系统 层面的 【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为 是可运行) - `BLOCKED` ,` WAITING` ,` TIMED_WAITING` 都是 Java API 层面对【阻塞状态】的细分,后面会在状态转换一节 详述 - `TERMINATED` 当线程代码运行结束 ## 4. 共享模型之管程 ### 4.1 共享带来的问题 - 老王(操作系统)有一个功能强大的算盘(CPU),现在想把它租出去,赚一点外快 ![image-20230418195639579](https://woldier-pic-repo-1309997478.cos.ap-chengdu.myqcloud.com/woldier/2023/04/334f3539aab545885fb54140ddc7c415.png) - 小南、小女(线程)来使用这个算盘来进行一些计算,并按照时间给老王支付费用 - 但小南不能一天24小时使用算盘,他经常要小憩一会(sleep),又或是去吃饭上厕所(阻塞 io 操作),有时还需要一根烟,没烟时思路全无(wait)这些情况统称为(阻塞) ![image-20230418195718984](https://woldier-pic-repo-1309997478.cos.ap-chengdu.myqcloud.com/woldier/2023/04/306f9aaf841c1c583e76d7bac2fc749c.png) - 在这些时候,算盘没利用起来(不能收钱了),老王觉得有点不划算 - 另外,小女也想用用算盘,如果总是小南占着算盘,让小女觉得不公平 - 于是,老王灵机一动,想了个办法 [ 让他们每人用一会,轮流使用算盘 ] - 这样,当小南阻塞的时候,算盘可以分给小女使用,不会浪费,反之亦然 - 最近执行的计算比较复杂,需要存储一些中间结果,而学生们的脑容量(工作内存)不够,所以老王申请了 一个笔记本(主存),把一些中间结果先记在本上 - 计算流程是这样的 ![image-20230418195813165](https://woldier-pic-repo-1309997478.cos.ap-chengdu.myqcloud.com/woldier/2023/04/a0931364c0f7c2ad1b98da1ace7c4ac5.png) - 但是由于分时系统,有一天还是发生了事故 - 小南刚读取了初始值 0 做了个 +1 运算,还没来得及写回结果 - 老王说 [ 小南,你的时间到了,该别人了,记住结果走吧 ],于是小南念叨着 [ 结果是1,结果是1...] 不甘心地 到一边待着去了(上下文切换) - 老王说 [ 小女,该你了 ],小女看到了笔记本上还写着 0 做了一个 -1 运算,将结果 -1 写入笔记本 - 这时小女的时间也用完了,老王又叫醒了小南:[小南,把你上次的题目算完吧],小南将他脑海中的结果 1 写 入了笔记本 ![image-20230418195909291](https://woldier-pic-repo-1309997478.cos.ap-chengdu.myqcloud.com/woldier/2023/04/869d00d1555e4cd3131999eb4e308002.png) 小南和小女都觉得自己没做错,但笔记本里的结果是 1 而不是 0 java实现 两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗? ```java static int counter = 0; public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { for (int i = 0; i < 5000; i++) { counter++; } }, "t1"); Thread t2 = new Thread(() -> { for (int i = 0; i < 5000; i++) { counter--; } }, "t2"); t1.start(); t2.start(); t1.join(); t2.join(); log.debug("{}", counter); } ``` 问题分析 以上的结果可能是正数、负数、零。为什么呢?因为 Java 中对静态变量的自增,自减并不是原子操作,要彻底理 解,必须从字节码来进行分析 例如对于 i++ 而言(i 为静态变量),实际会产生如下的 JVM 字节码指令: ![image-20230418200924933](https://woldier-pic-repo-1309997478.cos.ap-chengdu.myqcloud.com/woldier/2023/04/889a14fc017d4f0f554fc646de013297.png) 而对应 i-- 也是类似: ![image-20230418200940315](https://woldier-pic-repo-1309997478.cos.ap-chengdu.myqcloud.com/woldier/2023/04/3f41f055bba53d9035f3938d89d261d1.png) 而 Java 的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换: ![image-20230418201000592](https://woldier-pic-repo-1309997478.cos.ap-chengdu.myqcloud.com/woldier/2023/04/940f1e2495f337fd08ff77716b04746a.png) 如果是单线程以上 8 行代码是顺序执行(不会交错)没有问题: ![image-20230418201023519](https://woldier-pic-repo-1309997478.cos.ap-chengdu.myqcloud.com/woldier/2023/04/99d7c92692d3bf9bdc7aafd491cb1e54.png) 但多线程下这 8 行代码可能交错运行: 出现负数的情况: ![image-20230418201041843](https://woldier-pic-repo-1309997478.cos.ap-chengdu.myqcloud.com/woldier/2023/04/0e61c4fccfd35eaf266da735ee34ef54.png) ![image-20230418201258190](https://woldier-pic-repo-1309997478.cos.ap-chengdu.myqcloud.com/woldier/2023/04/18dcb8a667e3e249f3a573b30521c580.png) **临界区Ctitical section** - 一个程序运行多个线程本身是没有问题的 - 问题出在多个线程访问共享资源 - 多个线程读共享资源其实也没有问题 - 在多个线程对共享资源读写操作时发生指令交错,就会出现问题 - 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区 例如,下面代码中的临界区 ```java static int counter = 0; static void increment() // 临界区 { counter++; } static void decrement() // 临界区 { counter--; } ``` **竞态条件 Race Condition** 多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件 ### 4.2 synchronized解决方法 为了避免临界区的竞态条件发生,有多种手段可以达到目的。 - 阻塞式的解决方案:synchronized,Lock - 非阻塞式的解决方案:原子变量 本次课使用阻塞式的解决方案:synchronized,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一 时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁 的线程可以安全的执行临界区内的代码,不用担心线程上下文切换 > 注意 虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的: > > - 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码 > - 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点 **synchronized** ```java synchronized(对象){ //线程1,线程2 //临界区 } ``` 解决 ```java package com.ch4; import lombok.extern.slf4j.Slf4j; import java.util.Arrays; /** * @author woldier * @version 1.0 * @description synchronized * @date 2023/4/18 20:19 **/ @Slf4j(topic = "t.ch4") public class Demo01 { static int count = 5000; static final Object room = new Object(); public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { for (int i = 0; i < 5000; i++) { synchronized (room){ count--; } } },"减法器"); Thread t2 = new Thread(() -> { for (int i = 0; i < 5000; i++) { synchronized (room){ count++; } } },"减法器"); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(count); } } ``` ![image-20230418202707480](https://woldier-pic-repo-1309997478.cos.ap-chengdu.myqcloud.com/woldier/2023/04/faa36451042a47b296b7c0eac8eb5cb3.png) 你可以做这样的类比: - synchronized(对象) 中的对象,可以想象为一个房间(room),有唯一入口(门)房间只能一次进入一人 进行计算,线程 t1,t2 想象成两个人 - 当线程 t1 执行到 synchronized(room) 时就好比 t1 进入了这个房间,并锁住了门拿走了钥匙,在门内执行 count++ 代码 - 这时候如果 t2 也运行到了 synchronized(room) 时,它发现门被锁住了,只能在门外等待,发生了上下文切 换,阻塞住了 - 这中间即使 t1 的 cpu 时间片不幸用完,被踢出了门外(不要错误理解为锁住了对象就能一直执行下去哦), 这时门还是锁住的,t1 仍拿着钥匙,t2 线程还在阻塞状态进不来,只有下次轮到 t1 自己再次获得时间片时才 能开门进入 - 当 t1 执行完 synchronized{} 块内的代码,这时候才会从 obj 房间出来并解开门上的锁,唤醒 t2 线程把钥匙给他。t2 线程这时才可以进入 obj 房间,锁住了门拿上钥匙,执行它的 count-- 代码 ![image-20230418202830870](https://woldier-pic-repo-1309997478.cos.ap-chengdu.myqcloud.com/woldier/2023/04/12640a7d3abf60bbe539cc4f211823c6.png) synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切 换所打断。 为了加深理解,请思考下面的问题 - 如果把 synchronized(obj) 放在 for 循环的外面,如何理解?-- 原子性 这样想到与for循环也变成了不可分割的一部分 - 如果 t1 synchronized(obj1) 而 t2 synchronized(obj2) 会怎样运作?-- 锁对象 无法保证最后的结果正确,因为他们锁的对象不是同一个 - 如果 t1 synchronized(obj) 而 t2 没有加会怎么样?如何理解?-- 锁对象 如果t1在计算完成未写入的情况下下处理器调度,此时t2上处理器调度并且执行了一个完整的计算写入过程,那么在t1再次上处理器调度的时候则会将旧数据写入此时,会出现线程安全问题. **面对对象的改进** 把需要保护的共享变量放入一个类中 ```java package com.ch4; import lombok.extern.slf4j.Slf4j; /** * @author woldier * @version 1.0 * @description synchronized 面对对象 * @date 2023/4/19 11:03 **/ @Slf4j(topic = "c.Thread") public class Demo02 { public static void main(String[] args) throws InterruptedException { Room room = new Room(); Thread t1 = new Thread(() -> { for (int i = 0; i < 5000; i++) { room.inc(); } },"减法器"); Thread t2 = new Thread(() -> { for (int i = 0; i < 5000; i++) { room.dec(); } },"减法器"); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(room.getCount()); } } class Room { private int count = 5000; /** * description 加锁的inc * * @return void * @author: woldier * @date: 2023/4/19 11:07 */ public void inc() { synchronized (this) { //对当前类的实体对象加锁 count++; } } /** * description 加锁的dec * * @return void * @author: woldier * @date: 2023/4/19 11:07 */ public void dec() { synchronized (this) { count--; } } public int getCount(){ synchronized (this) { return count; } } } ``` ### 4.3 方法上的synchronized ```java class Test{ public synchronized void test() { } } 等价于 加在方法上的synchronized相当于锁住的this对象 class Test { public void test() { synchronized(this) { } } } ``` ```java class Test{ public synchronized static void test() { } } 等价于 加在静态方法上锁住的是类对象Class class Test { public static void test() { synchronized(this) { } } } ``` **面试所谓的"线程八锁"** 其实就是考察synchronized锁住的对象是哪一个 - 情况1: 输出 12 或21 ```java @Slf4j(topic = "c.Number") class Number { public synchronized void a() { log.debug("1"); } public synchronized void b() { log.debug("2"); } } public static void main(String[] args) { Number n1 = new Number(); new Thread(() -> { n1.a(); }).start(); new Thread(() -> { n1.b(); }).start(); } ``` 锁住的是`Number` new出来的实体对象`n1` - 情况2: 1s后1,2 ,或2 ,1s后1 ```java @Slf4j(topic = "c.Number") class Number { public synchronized void a() { sleep(1); log.debug("1"); } public synchronized void b() { log.debug("2"); } } public static void main(String[] args) { Number n1 = new Number(); new Thread(() -> { n1.a(); }).start(); new Thread(() -> { n1.b(); }).start(); } ``` - 情况3: 3 1s 12 或 23 1s 1 或 32 1s 1 ```java @Slf4j(topic = "c.Number") class Number { public synchronized void a() { sleep(1); log.debug("1"); } public synchronized void b() { log.debug("2"); } public void c() { log.debug("3"); } } public static void main(String[] args) { Number n1 = new Number(); new Thread(() -> { n1.a(); }).start(); new Thread(() -> { n1.b(); }).start(); new Thread(() -> { n1.c(); }).start(); } ``` 输出的可能性如下: 1. 3,先sleep1s,输出1再输出2 2. 输出2,3 sleep1s 输出1 3. 输出3,2 sleep1s输出1 - 情况4:2 1s 后 1 ```java @Slf4j(topic = "c.Number") class Number { public synchronized void a() { sleep(1); log.debug("1"); } public synchronized void b() { log.debug("2"); } } public static void main(String[] args) { Number n1 = new Number(); Number n2 = new Number(); new Thread(() -> { n1.a(); }).start(); new Thread(() -> { n2.b(); }).start(); } ``` 由于两个线程调用的函数不属于同一个类对象,因此不存在互斥 结果为 先输出2 sleep1s再输出1 - 情况5:2 1s 后 1 ```java @Slf4j(topic = "c.Number") class Number { public synchronized static void a() { sleep(1); log.debug("1"); } public synchronized void b() { log.debug("2"); } } public static void main(String[] args) { Number n1 = new Number(); new Thread(() -> { n1.a(); }).start(); new Thread(() -> { n1.b(); }).start(); } ``` 由于此时`b()`是`Number`类的静态方法,因此它所得是Number 类对象而不是Number对象new出来的实体`n1`因此执行结果为 输出2 sleep1s 输出1 - 情况6:1s 后12, 或 2 1s后 1 ```java @Slf4j(topic = "c.Number") class Number { public synchronized static void a() { sleep(1); log.debug("1"); } public synchronized static void b() { log.debug("2"); } } public static void main(String[] args) { Number n1 = new Number(); new Thread(() -> { n1.a(); }).start(); new Thread(() -> { n1.b(); }).start(); } ``` 由于 都是静态方法因此锁住的是`Number.class`对象 结果为 先输出2 sleep1s 输出1 或者先sleep1s 输出1 再输出2 - 情况7: 2 1s后1 ```java @Slf4j(topic = "c.Number") class Number { public synchronized static void a() { sleep(1); log.debug("1"); } public synchronized void b() { log.debug("2"); } } public static void main(String[] args) { Number n1 = new Number(); Number n2 = new Number(); new Thread(() -> { n1.a(); }).start(); new Thread(() -> { n2.b(); }).start(); } ``` - 情况8: 1s后1,2 或者 2 1s后1 ```java @Slf4j(topic = "c.Number") class Number { public synchronized static void a() { sleep(1); log.debug("1"); } public synchronized static void b() { log.debug("2"); } } public static void main(String[] args) { Number n1 = new Number(); Number n2 = new Number(); new Thread(() -> { n1.a(); }).start(); new Thread(() -> { n2.b(); }).start(); } ``` ### 4.4 变量的线程安全分析 - 成员变量和静态变量是否线程安全? 如果它们没有共享,则线程安全 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况 如果只有读操作,则线程安全 如果有读写操作,则这段代码是临界区,需要考虑线程安全 - 局部变量是否线程安全? 局部变量是线程安全的 但局部变量引用的对象则未必 如果该对象没有逃离方法的作用访问,它是线程安全的 如果该对象逃离方法的作用范围,需要考虑线程安全 局部变量线程安全分析 ```java public static void test1() { int i = 10; i++; } ``` 每个线程调用 test1() 方法时局部变量 i,会在每个线程的栈帧内存中被创建多份,因此不存在共享 ```java public static void test1(); descriptor: ()V flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=1, args_size=0 0: bipush 10 2: istore_0 3: iinc 6: return LineNumberTable: line 10: 0 line 11: 3 line 12: 6 LocalVariableTable: Start Length Slot Name Signature 3 ``` ![image-20230419143806920](https://woldier-pic-repo-1309997478.cos.ap-chengdu.myqcloud.com/woldier/2023/04/90c72d3e4f9c2cb7a8fd404ab7756d30.png) **局部变量的引用稍有不同** 先看一个成员变量的例子 ```java class ThreadUnsafe { ArrayList list = new ArrayList<>(); public void method1(int loopNumber) { for (int i = 0; i < loopNumber; i++) { // { 临界区, 会产生竞态条件 method2(); method3();// } 临界区 } } private void method2 () { list.add("1"); } private void method3 () { list.remove(0); } } ``` 执行 ![image-20230419151108494](https://woldier-pic-repo-1309997478.cos.ap-chengdu.myqcloud.com/woldier/2023/04/fccbdd1427db9337a54739c8fd0f3dd5.png) 其中一种情况是,如果线程2 还未 add,线程1 remove 就会报错: ![image-20230419151247120](https://woldier-pic-repo-1309997478.cos.ap-chengdu.myqcloud.com/woldier/2023/04/cb3ec19725d60ba1f35bd2af68ff3d00.png) 分析: - 无论那个线程中的method2引用的都是同一个对象的list成员对象 - method3与method2分析相同 原因是list底层的size 就是i++ ,i-- ```java new Thread(() -> { list.add("1"); // 时间1. 会让内部 size ++ list.remove(0); // 时间3. 再次 remove size-- 出现角标越界 }, "t1").start(); new Thread(() -> { list.add("2"); // 时间1(并发发生). 会让内部 size ++,但由于size的操作非原子性, size 本该是2,但结果可能出现1 list.remove(0); // 时间2. 第一次 remove 能成功, 这时 size 已经是0 }, "t2").start(); ``` ![image-20230419154524355](https://woldier-pic-repo-1309997478.cos.ap-chengdu.myqcloud.com/woldier/2023/04/749ea5439d671675bacec8d2fba7f99e.png) 将list修改为局部变量就不会出现上述问题了 ```java class ThreadUnsafe { public void method1(int loopNumber) { ArrayList list = new ArrayList<>(); for (int i = 0; i < loopNumber; i++) { // { 临界区, 会产生竞态条件 method2(list); method3(list);// } 临界区 } } private void method2 (ArrayList list) { list.add("1"); } private void method3 (ArrayList list) { list.remove(0); } } ``` 分析: - list 是局部变量,每个线程调用时会创建其不同实例,没有共享 - 而 method2 的参数是从 method1 中传递过来的,与 method1 中引用同一个对象 - method3 的参数分析与 method2 相同 ![image-20230419154743659](https://woldier-pic-repo-1309997478.cos.ap-chengdu.myqcloud.com/woldier/2023/04/4ceed9c42a265a216ccc1a9221cde4de.png) 方法访问修饰符带来的思考,如果把 method2 和 method3 的方法修改为 public 会不会代理线程安全问题? - 情况1:有其它线程调用 method2 和 method3 - 情况2:在 情况1 的基础上,为 ThreadSafe 类添加子类,子类覆盖 method2 或 method3 方法,即 ```java class ThreadSafe { public final void method1(int loopNumber) { ArrayList list = new ArrayList<>(); for (int i = 0; i < loopNumber; i++) { method2(list); method3(list); } } private void method2(ArrayList list) { list.add("1"); } private void method3(ArrayList list) { list.remove(0); } } class ThreadSafeSubClass extends ThreadSafe { @Override public void method3(ArrayList list) { //重写了方法并使用其他线程来操作局部变量 new Thread(() -> { list.remove(0); }).start(); } } ``` 这里的`ThreadSafeSubClass`继承重写了`ThreadSafe`的`method3`这导致了`ThreadSafe`中的list局部变量被多个线程访问,因此是线程不安全的 > 从这个例子可以看出 private 或 final 提供【安全】的意义所在,请体会开闭原则中的【闭】 **常见线程安全类** - String - Integer - StringBuffer - Random - Vector - Hashtable - java.util.concurrent 包下的类 这里说它们是线程安全的是指,多个线程调用他们同一个实例的某个方法时,是线程安全的.也可以理解为 ```java Hashtable table = new Hashtable(); new Thread(()->{ table.put("key", "value1");}).start(); new Thread(()->{ table.put("key", "value2");}).start(); ``` 它们的每个方法是原子的 但注意它们多个方法的组合不是原子的,见后面分析 **线程安全类方法的组合** 分析下面代码是否是线程安全的? ```java Hashtable table = new Hashtable(); // 线程1,线程2 if( table.get("key") == null) { table.put("key", value); } ``` ![image-20230419160247387](https://woldier-pic-repo-1309997478.cos.ap-chengdu.myqcloud.com/woldier/2023/04/21c37eea9c1c76909eb65d148a2cf609.png) 两个线程按照如上情况进行则是线程不安全的 **不可变类线程安全性** String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的 有同学或许有疑问,String 有 replace,substring 等方法【可以】改变值啊,那么这些方法又是如何保证线程安 全的呢? **实例分析** ```java public class MyServlet extends HttpServlet { // 是否安全? Map map = new HashMap<>(); // 是否安全? String S1 = "..."; // 是否安全? final String S2 = "..."; // 是否安全? Date D1 = new Date(); // 是否安全? final Date D2 = new Date(); public void doGet(HttpServletRequest request, HttpServletResponse response) { } } ``` 例2: ```java public class MyServlet extends HttpServlet { // 是否安全? private UserService userService = new UserServiceImpl(); public void doGet(HttpServletRequest request, HttpServletResponse response) { userService.update(...); } } public class UserServiceImpl implements UserService { // 记录调用次数 private int count = 0; public void update() { // ... count++; } } ``` 例3: ```java @Aspect @Component public class MyAspect { // 是否安全? // private long start = 0L; @Before("execution(* *(..))") public void before() { start = System.nanoTime(); } @After("execution(* *(..))") public void after() { long end = System.nanoTime(); System.out.println("cost time:" + (end - start)); } } ``` 不安全的,因为spring 中默认都是单例,这样这个开始时间start 就是共享变量,存在线程安全问题,解决办法是通过ThreadLocal存储变量. 那是否将scope改为prototype可解决呢,答案是也不行的,因为我们不能保证前置和后置调用的都是一样的 或者是 直接将其作为一个环绕通知 即可 例4: ```java public class MyServlet extends HttpServlet { // 是否安全 private UserService userService = new UserServiceImpl(); public void doGet(HttpServletRequest request, HttpServletResponse response) { userService.update(...); } } public class UserServiceImpl implements UserService { // 是否安全 // private UserDao userDao = new UserDaoImpl(); public void update() { userDao.update(); } } public class UserDaoImpl implements UserDao { public void update() { String sql = "update user set password = ? where username = ?"; // 是否安全 try (Connection conn = DriverManager.getConnection("", "", "")) { // ... } catch (Exception e) { // ... } } } ``` 典型的三层架构写法,是线程安全的,但是 如果这个sql语句是inseart 可能出现问题 例5: ```java public class MyServlet extends HttpServlet { // 是否安全 private UserService userService = new UserServiceImpl(); public void doGet(HttpServletRequest request, HttpServletResponse response) { userService.update(...); } } public class UserServiceImpl implements UserService { // 是否安全 // private UserDao userDao = new UserDaoImpl(); public void update() { userDao.update(); } } public class UserDaoImpl implements UserDao { private Connection = null; public void update() { String sql = "update user set password = ? where username = ?"; // 是否安全 try ( conn = DriverManager.getConnection("", "", "")) { // ... } catch (Exception e) { // ... } } } ``` 由于UserDaoImpl的成员变量被多个线程所共享,因此存在问题 例6: ```java public class MyServlet extends HttpServlet { // 是否安全 private UserService userService = new UserServiceImpl(); public void doGet(HttpServletRequest request, HttpServletResponse response) { userService.update(...); } } public class UserServiceImpl implements UserService { // 是否安全 public void update() { private UserDao userDao = new UserDaoImpl(); userDao.update(); } } public class UserDaoImpl implements UserDao { private Connection = null; public void update() { String sql = "update user set password = ? where username = ?"; // 是否安全 try ( conn = DriverManager.getConnection("", "", "")) { // ... } catch (Exception e) { // ... } } } ``` 虽然`UserDaoImpl`存在可被共享的变量 但是在`UserServiceImpl`每一个线程都是自己new的一个`UserDaoImpl`因此不存在线程安全问题 例7: ```java public abstract class Test { public void bar() { // 是否安全 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); foo(sdf); } public abstract foo(SimpleDateFormat sdf); public static void main(String[] args) { new Test().bar(); } } ``` 其中 foo 的行为是不确定的,可能导致不安全的发生,被称之为外星方法 ```java public void foo(SimpleDateFormat sdf) { String dateStr = "1999-10-11 00:00:00"; for (int i = 0; i < 20; i++) { new Thread(() -> { try { sdf.parse(dateStr); } catch (ParseException e) { e.printStackTrace(); } }).start(); } } ``` 请比较 JDK 中 String 类的实现 例8: ```java private static Integer i = 0; public static void main(String[] args) throws InterruptedException { List list = new ArrayList<>(); for (int j = 0; j < 2; j++) { Thread thread = new Thread(() -> { for (int k = 0; k < 5000; k++) { synchronized (i) { i++; } } }, "" + j); list.add(thread); } list.stream().forEach(t -> t.start()); list.stream().forEach(t -> { try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } }); log.debug("{}", i); } ``` ### 4.5 习题 买票问题 ```java package com.ch4; import lombok.extern.slf4j.Slf4j; import java.util.List; import java.util.Random; import java.util.Vector; /** * @author woldier * @version 1.0 * @description 买票问题 线程不安全 * @date 2023/4/19 16:52 **/ @Slf4j(topic = "c.Thread") public class Demo04 { public static void main(String[] args) { TicketWindow ticketWindow = new TicketWindow(2000); List threads = new Vector<>(); //存放线程 List count = new Vector<>(); //存放买票数目,这里没有使用int 类型近累加是因为可能出现先线程安全问题 for (int i = 0; i < 20000; i++) { Thread thread = new Thread( () -> count.add(ticketWindow.getTicket(randomAmount())) ); threads.add(thread); thread.start(); } threads.forEach(e -> { //线程jion try { e.join(); } catch (InterruptedException ex) { throw new RuntimeException(ex); } }); log.debug("卖出的总数为 {}",count.stream().mapToInt(e -> e).sum() ); log.debug("剩余的票数为{}",ticketWindow.getCount()); } // Random 为线程安全 static Random random = new Random(); // 随机 1~5 public static int randomAmount() { return random.nextInt(5) + 1; } } class TicketWindow { private int count; public int getCount() { return count; } public TicketWindow(int count) { this.count = count; } public int getTicket(int num) { if (count >= num) { count = count - num; return num; } else return 0; } } ``` ```shell 17:08:03 [main] c.Thread - 卖出的总数为 2001 17:08:03 [main] c.Thread - 剩余的票数为1 Process finished with exit code 0 ``` 买超了,存在线程安全问题 转账 ```java @Slf4j(topic = "c.Thread") public class Demo05 { public static void main(String[] args) throws InterruptedException { Account a = new Account(1000); Account b = new Account(1000); Thread t1 = new Thread(() -> { for (int i = 0; i < 1000; i++) { a.transfer(b, randomAmount()); } }, "t1"); Thread t2 = new Thread(() -> { for (int i = 0; i < 1000; i++) { b.transfer(a, randomAmount()); } }, "t2"); t1.start(); t2.start(); t1.join(); t2.join(); // 查看转账2000次后的总金额 log.debug("total:{}", (a.getMoney() + b.getMoney())); } // Random 为线程安全 static Random random = new Random(); // 随机 1~100 public static int randomAmount() { return random.nextInt(100) + 1; } } class Account { private int money; public Account(int money) { this.money = money; } public int getMoney() { return money; } public void setMoney(int money) { this.money = money; } public void transfer(Account target, int amount) { if (this.money > amount) { this.setMoney(this.getMoney() - amount); target.setMoney(target.getMoney() + amount); } } } ``` 改正方法是 ```java public void transfer(Account target, int amount) { synchronized(Account.class){ if (this.money > amount) { this.setMoney(this.getMoney() - amount); target.setMoney(target.getMoney() + amount); } } } ``` 如果是` synchronized(this)` 还是会造成数据不一致 ### 4.6 Monitor 对象头 以 32 位虚拟机为例 普通对象 ![image-20230419172940676](https://woldier-pic-repo-1309997478.cos.ap-chengdu.myqcloud.com/woldier/2023/04/bf336e4626509d3bc77f6d3f16be45b4.png) 数组对象 ![image-20230419172959290](https://woldier-pic-repo-1309997478.cos.ap-chengdu.myqcloud.com/woldier/2023/04/ee25821775c4483a918693f1cf6753e4.png) 其中 Mark Word 结构为 ![image-20230419173020539](https://woldier-pic-repo-1309997478.cos.ap-chengdu.myqcloud.com/woldier/2023/04/366c9b66a0182111bfa831ff15822498.png) 64 位虚拟机 Mark Word ![image-20230419173040063](https://woldier-pic-repo-1309997478.cos.ap-chengdu.myqcloud.com/woldier/2023/04/14db56bd865157bf028641d77f0b1419.png) https://stackoverflow.com/questions/26357186/what-is-in-java-object-header **原理解释** https://www.bilibili.com/video/BV16J411h7Rd/?p=76&spm_id_from=pageDriver&vd_source=b592fd0fd3bd041bab6398e89668385d **字节码角度** https://www.bilibili.com/video/BV16J411h7Rd/?p=77&spm_id_from=pageDriver&vd_source=b592fd0fd3bd041bab6398e89668385d ![image-20230419174535633](https://woldier-pic-repo-1309997478.cos.ap-chengdu.myqcloud.com/woldier/2023/04/fa8defea744567776ab20f803adfaedb.png)