135.【JUC并发编程_01】
JUC 并发编程
- (一)、基本概述
- 1.概述
- (二)、进程与线程
- 1.进程与线程
- (1).进程_介绍
- (2).线程_介绍
- (3).进程与线程的区别
- 2.并行和并发
- (1).并发_介绍
- (2).并行_介绍
- (3).并行和并发的区别
- 3.应用
- (1).异步调用_较少等待时间
- (2).多线程_提高效率
- (三)、Java 线程
- 1.创建线程和运行线程
- (1).直接使用 Thread (第一种)
- (2).使用Runnable配合Thread (第二种)
- (3).lamda优化线程创建
- (4).Thread 和 Runnable的原理
- (5).Thread 和 Runnable 的总结
- (6).FutureTask 配合 Thread
- (7).观察_多线程运行情况
- 2.查看进程线程的方法
- (1).Winodws 操作系统
- (2).Linux 操作系统
- (3).Java
- 3.栈与栈帧
- (1). 栈与栈帧
- (2).线程上下文切换 (Thread Context Switch)
- 4.线程方法
- (1).start() 与 run() _(Runnable->Running)
- (2).sleep() _(Running->Blocked)
- (3).yield() _(Running->Runnable)
- (4).线程优先级 和 yield()
- (5).sleep应用_防止CPU占用100%
- (6).join_等待线程运行结束
- (7).join_同步应用
- (8).interrupt_(RUNNING->Waitting)
- (9).interrupt_两阶段终止
- (10).park_打断线程
- (11).不推荐的打断方法
- 5.主线程与守护线程
- (1).守护线程
- 6.线程状态
- (1).五种状态_操作系统
- (2).六种状态_Java API
- (3).线程六种状态演示
- 7.统筹规划_分析
- (1).问题定义
- (2).代码展示
- (四)、共享模型之管程
- 1.共享问题
- (1).小故事
- (2).Java 的体现
- (3).问题分析
- (4).临界区 Critical Section
- (5).竞态条件 Race Condition
- 2. synchronized 解决方案
- (1).应用之互斥
- (2).synchronized 对象锁
- (3).synchrpnized_理解
- (4).synchorized_思考
- 3.synchorized_面向对象改进
- (1).面向对象改进
- 4.synchorized_作用域
- (1).添加在非静态方法上 _ 锁住的是this
- (2).添加在静态方法上 _ 锁住的是类对象
- (3).不加 synchronized 的方法
- 5. 所谓的"线程八锁"
- (1).情况1_ 两个都没有睡眠
- (2).情况2_其中一个有睡眠
- (3).情况3_存在一个没有加锁的方法
- (4).情况4_锁同类两个不同对象
- (5).情况5_锁一个静态方法
- (6).情况6_锁两个静态方法
- (7).情况7_锁同类两个不同对象+锁一个静态方法
- (8).情况8_锁同类两个不同对象+锁两个静态方法
- 6.线程安全分析
- (1).成员变量和静态变量是否线程安全?
- (2).局部变量是否线程安全?
- (3).局部变量线程安全分析
- (4).常见线程安全类
- (4.1). 线程安全类方法的组合
- (4.2).不可变类线程安全性
- (5).实例分析
- 7.习题
- (1).卖票练习
- (2).卖票解题_受保护的是一个实列对象
- (3).转账练习_受保护的是多个实列对象
- 8. Monitor 概念
- (1).Java 对象头
- (2).Monitor_synorized原理
- (3).Monitor_字节码角度
- (4).小故事
- (5).synchorized优化原理_轻量级锁 (解决防盗锁)
- (6).synchorized优化原理_锁膨胀
- (7).synchorized优化原理_自旋锁
- (8).synchorized优化原理_偏向锁 (解决书包翻书)
- (9).偏向锁细讲_状态
- (10).撤销偏向锁 - 调用对象 hashCode
- (11).撤销偏向锁 - 其它线程使用对象
- (12).撤销偏向锁 - 调用 wait/notify
- (13).批量偏向锁 - 批量对象
- (14).批量撤销锁 - 批量对象
- (15).锁消除
- (15).锁粗化
(一)、基本概述
1.概述
- 希望你不是一个初学者
- 线程安全问题,需要你接触过 Java Web 开发、Jdbc 开发、Web 服务器、分布式框架时才会遇到
- 基于 JDK 8,最好对函数式编程、lambda 有一定了解
- 采用了 slf4j 打印日志,这是好的实践
- 采用了 lombok 简化 java bean 编写
- 给每个线程好名字,这也是一项好的实践
1.pm.xml依赖如下
<properties><maven.compiler.source>1.8</maven.compiler.source><maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.10</version></dependency><dependency><groupId>ch.qos.logback</groupId><artifactId>logback-classic</artifactId><version>1.2.3</version></dependency>
</dependencies>
2.logback.xml 配置如下
<?xml version="1.0" encoding="UTF-8"?>
<configurationxmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://ch.qos.logback/xml/ns/logback "><appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"><encoder><pattern>%date{HH:mm:ss} [%t] %logger - %m%n</pattern></encoder></appender><logger name="c" level="debug" additivity="false"><appender-ref ref="STDOUT"/></logger><root level="ERROR"><appender-ref ref="STDOUT"/></root>
</configuration>
(二)、进程与线程
1.进程与线程
(1).进程_介绍
- 程序由指令和数据组成,但是这样指令要运行,数据要读写,就必须将指令加载到CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理IO的。
- 当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
- 进程就可以视为程序的一个实列。大部分程序可以同时运行多个实列进程(列如记事本、图画、浏览器等),也有的程序只能启动一个实列进程(列如 网易云音乐、360安全卫士)
(2).线程_介绍
- 一个进程之内可以分为一到多个线程。
- 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行。
Java中,线程作为最小调度单位,进程作为资源分配的最小单位
。在winodw中进程是不活动的,只是作为线程的容器。
(3).进程与线程的区别
-
进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
-
进程拥有共享的资源,如内存空间等,供其内部的线程共享
-
进程间通信较为复杂
同一台计算机的进程通信称为 IPC
(Inter-process communication)不同计算机之间的进程通信,需要通过网络,并遵守共同的协议
,例如 HTTP
-
线程通信相对简单,因为它们共享进程内的内存
,一个例子是多个线程可以访问同一个共享变量 -
线程更轻量,
线程上下文切换成本一般上要比进程上下文切换低
2.并行和并发
并行:
小海王左手牵着A女友同时右手牵着B女友。
并发:
老海王分时间安排和不同的女友见面,两个女友看不出任何破绽。时间管理大师!!!
(1).并发_介绍
单核 cpu 下,线程实际还是 串行执行
的。操作系统中有一个组件叫做任务调度器,将 cpu 的时间片
(windows
下时间片最小约为 15 毫秒)分给不同的程序使用,只是由于 cpu 在线程间(时间片很短)的切换非常快,人类感觉是 同时运行的
。总结为一句话就是: 微观串行,宏观并行 。
一般会将这种 线程轮流使用 CPU
的做法称为并发, concurrent
(2).并行_介绍
多核 cpu下,每个 核(core)
都可以调度运行线程,这时候线程可以是并行的。
极致的并发就是并行。
(3).并行和并发的区别
- 并发(
concurrent
)是同一时间应对(dealing with)多件事情的能力 - 并行(
parallel
)是同一时间动手做(doing)多件事情的能力
3.应用
(1).异步调用_较少等待时间
从方法的角度来讲,如果
- 需要等待结果返回,才能继续运行就是同步。
- 不需要等待结果返回,就能继续运行就是异步。
注意: 同步子啊多线程中还有另外一层意思,是让多个线程步调一致。
- 设计
多线程可以让方法执行变为异步
的(即不要巴巴干等着)比如说读取磁盘文件时,假设读取操作花费了 5 秒钟,如果没有线程调度机制,这 5 秒 cpu 什么都做不了,其它代码都得暂停…
- 结论
- 比如在项目中,
视频文件需要转换格式等操作比较费时
,这时开一个新线程处理视频转换,避免阻塞主线程。(视频文件格式转换) - tomcat 的异步
servlet
也是类似的目的,让用户线程处理耗时较长的操作,避免阻塞 tomcat 的工作线程。(tomcat) - ui 程序中,开线程进行其他操作,
避免阻塞 ui 线程
。(ui)
(2).多线程_提高效率
充分利用多核 cpu 的优势,提高运行效率。想象下面的场景,执行 3 个计算,最后将计算结果汇总。
计算 1 花费 10 ms计算 2 花费 11 ms计算 3 花费 9 ms汇总需要 1 ms
- 如果是
串行执行
,那么总共花费的时间是10 + 11 + 9 + 1 = 31ms
- 但如果是
四核 cpu
,各个核心分别使用线程 1 执行计算 1,线程 2 执行计算 2,线程 3 执行计算 3,那么 3 个
线程是并行的,花费时间只取决于最长的那个线程运行的时间,即 11ms 最后加上汇总时间只会花费 12ms.
注意: 需要在多核 cpu 才能提高效率,单核仍然时是轮流执行
- 搭建测试环境: 分别在单核和多核下测试
1.向pom.xml中导入我们的JHM检测包工具
<dependency><groupId>org.openjdk.jmh</groupId><artifactId>jmh-core</artifactId><version>1.23</version></dependency><dependency><groupId>org.openjdk.jmh</groupId><artifactId>jmh-generator-annprocess</artifactId><version>1.23</version></dependency><!-- https://mvnrepository.com/artifact/cn.hutool/hutool-all -->
<dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.5</version>
</dependency>
2.编写测试的类
package com.jsxs.sample;/*** @Author Jsxs* @Date 2023/9/29 13:45* @PackageName:com.jsxs.sample* @ClassName: MyBenchmark* @Description: TODO* @Version 1.0*/import java.util.Arrays;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.Warmup;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;@Fork(1)
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 3,time = 1,timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5,time = 1,timeUnit = TimeUnit.SECONDS)
public class MyBenchmark {static int[] ARRAY = new int[1000_000_00];static {Arrays.fill(ARRAY, 1);}@Benchmarkpublic int c() throws Exception {int[] array = ARRAY;FutureTask<Integer> t1 = new FutureTask<>(() -> {int sum = 0;for (int i = 0; i < 250_000_00; i++) {sum += array[0 + i];}return sum;});FutureTask<Integer> t2 = new FutureTask<>(() -> {int sum = 0;for (int i = 0; i < 250_000_00; i++) {sum += array[250_000_00 + i];}return sum;});FutureTask<Integer> t3 = new FutureTask<>(() -> {int sum = 0;for (int i = 0; i < 250_000_00; i++) {sum += array[500_000_00 + i];}return sum;});FutureTask<Integer> t4 = new FutureTask<>(() -> {int sum = 0;for (int i = 0; i < 250_000_00; i++) {sum += array[750_000_00 + i];}return sum;});new Thread(t1).start();new Thread(t2).start();new Thread(t3).start();new Thread(t4).start();return t1.get() + t2.get() + t3.get() + t4.get();}@Benchmarkpublic int d() throws Exception {int[] array = ARRAY;FutureTask<Integer> t1 = new FutureTask<>(() -> {int sum = 0;for (int i = 0; i < 1000_000_00; i++) {sum += array[0 + i];}return sum;});new Thread(t1).start();return t1.get();}public static void main(String[] args) throws RunnerException {Options opt = new OptionsBuilder().include(MyBenchmark.class.getSimpleName()).forks(1).build();new Runner(opt).run();}}
- 结论
- 单核 cpu 下,
多线程不能实际提高程序运行效率,只是为了能够在不同的任务之间切换
,不同线程轮流使用cpu ,不至于一个线程总占用 cpu,别的线程没法干活。 多核 cpu 可以并行跑多个线程,但能否提高程序运行效率还是要分情况的
- 有些任务,经过精心设计,将任务拆分,并行执行,当然可以提高程序的运行效率。但不是所有计算任务都能拆分(参考后文的【阿姆达尔定律】)
- 也不是所有任务都需要拆分,任务的目的如果不同,谈拆分和效率没啥意义
- IO 操作不占用 cpu,只是我们一般拷贝文件使用的是【阻塞 IO】,这时相当于线程虽然不用 cpu,但需要一直等待 IO 结束,没能充分利用线程。所以才有后面的【非阻塞 IO】和【异步 IO】优化。
(三)、Java 线程
1.创建线程和运行线程
(1).直接使用 Thread (第一种)
1.创建线程:
// 创建线程对象
Thread t = new Thread() {public void run() {// 要执行的任务}
};
// 启动线程
t.start();
2.使用线程列子
package com.jsxs.utils;import lombok.extern.slf4j.Slf4j;import java.io.FileReader;/*** @Author Jsxs* @Date 2023/9/29 13:20* @PackageName:com.jsxs.utils* @ClassName: Sync* @Description: TODO* @Version 1.0*/@Slf4j(topic = "c.Sync")
public class Sync {public static void main(String[] args) {// 1.构造方法的参数指定名字,推荐Thread thread = new Thread("t1") {@Overridepublic void run() {log.debug("hello");}};// 2. 开始交给任务处理器支配thread.start();}
}
3.输出:
(2).使用Runnable配合Thread (第二种)
把【线程】和【任务】(要执行的代码)分开
- Thread 代表线程
- Runnable 可运行的任务(线程要执行的代码)
1.创建任务并执行线程
Runnable runnable = new Runnable() {public void run(){// 要执行的任务}
};
// 创建线程对象
Thread t = new Thread( runnable );
// 启动线程
t.start();
2.执行线程
package com.jsxs.utils;import lombok.extern.slf4j.Slf4j;import java.io.FileReader;/*** @Author Jsxs* @Date 2023/9/29 13:20* @PackageName:com.jsxs.utils* @ClassName: Sync* @Description: TODO* @Version 1.0*/@Slf4j(topic = "c.Sync")
public class Sync {public static void main(String[] args) {// 1.创建执行任务,因为是一个接口需要实现方法Runnable runnable = new Runnable() {@Overridepublic void run() {log.debug("hello-Runnable");}};// 2.启动线程,并且给线程指定名字new Thread(runnable,"Runnable").start();}
}
(3).lamda优化线程创建
使用lamda表达式的条件为: 函数式接口(接口中只有一个方法)
package com.jsxs.utils;import lombok.extern.slf4j.Slf4j;import java.io.FileReader;/*** @Author Jsxs* @Date 2023/9/29 13:20* @PackageName:com.jsxs.utils* @ClassName: Sync* @Description: TODO* @Version 1.0*/@Slf4j(topic = "c.Sync")
public class Sync {public static void main(String[] args) {// 假如函数式接口中的方法没有参数,那么直接用()表示,返回值类型是void直接直接写函数的语句就行new Thread(()->log.debug("hello-lamda"),"lamda").start();}
}
(4).Thread 和 Runnable的原理
1. 两者实际上都是使用的Thread的run()方法进行操作的
(5).Thread 和 Runnable 的总结
- Thread 是把线程和任务合并在了一起,Runnable是把线程和任务分开了
- 用 Runnable 更容易与线程池等高级 API 配合。
- 用 Runnable 让任务类脱离了 Thread 继承体系,更灵活。
(6).FutureTask 配合 Thread
FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况:
package com.jsxs.utils;import lombok.extern.slf4j.Slf4j;import java.io.FileReader;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;/*** @Author Jsxs* @Date 2023/9/29 13:20* @PackageName:com.jsxs.utils* @ClassName: Sync* @Description: TODO* @Version 1.0*/@Slf4j(topic = "c.Sync")
public class Sync {public static void main(String[] args) throws ExecutionException, InterruptedException {// 1.编写执行任务,并设置此线程执行完毕后的返回值FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {@Overridepublic Integer call() throws Exception {log.debug("hello-futureTask");Thread.sleep(1000); //释放资源但不释放锁return 100;}});// 2.启动线程new Thread(task,"t3").start();// 3.调用get()方法会造成_主线程阻塞,同步等待 task 执行完毕的结果⭐⭐Integer integer = task.get();// 4. {}占位符,第一个占位符对应第一个参数....log.debug("结果是:{}",integer);}
}
(7).观察_多线程运行情况
package com.jsxs.utils;import lombok.extern.slf4j.Slf4j;import java.io.FileReader;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;/*** @Author Jsxs* @Date 2023/9/29 13:20* @PackageName:com.jsxs.utils* @ClassName: Sync* @Description: TODO* @Version 1.0*/@Slf4j(topic = "c.Sync")
public class Sync {public static void main(String[] args) throws ExecutionException, InterruptedException {new Thread(()->{while (true)log.debug("running1...","t1");}).start();new Thread(()->{while (true)log.debug("running2...","t2");}).start();}
}
2.查看进程线程的方法
(1).Winodws 操作系统
-
任务管理器查看进程和线程数,也可以用来杀死进程。
-
tasklist 查看进程。
tasklist |findstr java
-
taskkill 杀死进程。
taskill /F /PID pid号
(2).Linux 操作系统
ps -ef
查看所有的进程信息。ps -ef -p <pid>
查看某个进程pid的所有线程。netstat -anp|grep 端口号
查看某一个端口号是否开启。kill
杀死进程。top
按大写H切换是否显示线程。top -HP <pid>
查看某个进程pid的所有线程。
(3).Java
-
jps命令查看所有java进程。
-
jstack < pid > 查看某个Java进程(PID)的所有线程状态。
-
jconsole 来查看某个Java进程中线程的运行情况。 (图形化界面)
3.栈与栈帧
(1). 栈与栈帧
Java Virtual Machine Stacks (Java 虚拟机栈
)
我们都知道 JVM 中由堆、栈、方法区
所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟机就会为其分配一块栈内存。
- 每个栈内存由多个栈帧(Frame)组成,对应着每次
方法
调用时所占用的内存 - 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
- 单线程_栈与栈帧
package com.jsxs.utils;import lombok.extern.slf4j.Slf4j;import java.io.FileReader;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;/*** @Author Jsxs* @Date 2023/9/29 13:20* @PackageName:com.jsxs.utils* @ClassName: Sync* @Description: TODO* @Version 1.0*/@Slf4j(topic = "c.Sync")
public class Sync {public static void main(String[] args) throws ExecutionException, InterruptedException {method1(10);}public static void method1(int x){int y= x+1;Object o = method2();System.out.println(o);}public static Object method2(){Object n = new Object();return n;}}
一个线程对应一个栈内存,一个栈内存又多个栈帧组成!
当我们结果运行完毕后,栈帧和栈内存会被释放!!!
- 类加载过程
类加载过程示意图:
①.首先通过一个类的全限定名获取该类的二进制;然后将该二进制流中静态存储结构转化为方法区运行时结构;最后在栈内存中生成该类的class对象,作为该类数据的访问入口。
②.验证文件格式(字节流是否符合、主次版本号是否在虚拟机范围内、常量池中的常量是否右不被支持的类型)、验证元数据(对字节码描述的信息进行语义分析是否有父类)、验证字节码(验证数据流和控制流程序语义是否正确)、验证符号引用(主要是为了确保解析动作能正确执行)。
③.为类中的静态变量分配内存并初始化默认值,这些内存都是在方法区进行分配的;准备阶段不分配类中实列变量的内存
,实列变量将会在对象实列化时随着对象一起分配到堆中。
④.主要完成符号引用到直接引用的转换动作
⑤.初始化时类加载的最后一步,到了初始化阶段,才真正开始执行类中定义的Java程序代码。
- 栈帧图解
栈帧里面有: 局部变量表、返回地址、锁记录、操作数帧!
①通过二进制流转换成方法区运行时结构;②当我们启动项目之后会获取到时间片就会调用main线程(main内存);③main线程会生成main栈帧;④程序计算器负责指挥先main方法执行然后method1最后method2,⑤当程序运行结束后,清理顺序是先methos2、methods1最后是main方法。
- 多线程下_栈与栈帧
package com.jsxs.utils;import lombok.extern.slf4j.Slf4j;import java.io.FileReader;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;/*** @Author Jsxs* @Date 2023/9/29 13:20* @PackageName:com.jsxs.utils* @ClassName: Sync* @Description: TODO* @Version 1.0*/@Slf4j(topic = "c.Sync")
public class Sync {public static void main(String[] args) throws ExecutionException, InterruptedException {// 1.t1线程new Thread("t1"){@Overridepublic void run() {method1(20);}}.start();// 2.main线程method1(10);}public static void method1(int x){int y= x+1;Object o = method2();System.out.println(o);}public static Object method2(){Object n = new Object();return n;}}
(2).线程上下文切换 (Thread Context Switch)
因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码。
- 线程的
cpu
时间片用完。 垃圾回收
。- 有更高
优先级
的线程需要运行。 - 线程自己调用了
sleep
、yield
、wait
、join
、park
、synchronized
、lock
等方法。
当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态。Java 中对应的概念就是程序计数器(Program Counter Register)
,它的作用是记住下一条 jvm 指令的执行地址,是线程私有的。
- 状态包括
程序计数器
、虚拟机栈中每个栈帧的信息
,如局部变量
、操作数栈
、返回地址
等。 - Context Switch 频繁发生会影响性能。
- 保存的信息在线程控制块 (TCB)中。
- 线程数越多的话,会有越频繁的上下文切换,影响性能。
4.线程方法
(1).start() 与 run() _(Runnable->Running)
package com.jsxs.utils;import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;import java.io.FileReader;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;/*** @Author Jsxs* @Date 2023/9/29 13:20* @PackageName:com.jsxs.utils* @ClassName: Sync* @Description: TODO* @Version 1.0*/@Slf4j(topic = "c.Sync")
public class Sync {public static void main(String[] args) throws Exception{Thread thread = new Thread("t1") {@SneakyThrows@Overridepublic void run() {// 当线程t1被时间片获取的时候,就会执行打印log.debug("running...");// 这里模拟读取文件需要十秒种Thread.sleep(10000);}};System.out.println(thread.getState()); //查看线程状态
// thread.run(); //假如我们使用直接调用run,也能够启动线程并打印run方法里面的数据,但是不会生成t1线程,所有的操作都是main线程在操纵 (也就是说同步等待)thread.start(); // 使用这个开启线程之后,会生成一个t1线程去执行 run方法;main线程去执行其他的事情 (也就是说会异步)log.debug("do other things .....");}
}
结论:
- 使用run方法启动,实质上只有一个main线程。
- 使用start方法启动,会生成一个新的线程和main线程。
- start方法不能重复调用,否则会报异常。
(2).sleep() _(Running->Blocked)
- 调用
sleep
会让当前线程从Running
进入Timed Waiting
状态(阻塞) - 其它线程可以使用
interrupt
方法打断正在睡眠的线程,这时 sleep 方法会抛出InterruptedException
- 睡眠结束后的线程
未必会立刻
得到执行 - 建议用
TimeUnit 的 sleep
代替Thread 的 sleep
来获得更好的可读性
- sleep 进入阻塞状态…
1.调用sleep()会进入阻塞状态.....
package com.jsxs.utils;import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;import java.io.FileReader;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;/*** @Author Jsxs* @Date 2023/9/29 13:20* @PackageName:com.jsxs.utils* @ClassName: Sync* @Description: TODO* @Version 1.0*/@Slf4j(topic = "c.Sync")
public class Sync {public static void main(String[] args) throws Exception{Thread thread = new Thread("t1") {@SneakyThrows@Overridepublic void run() {// 当线程t1被时间片获取的时候,就会执行打印log.debug("running...");// 这里模拟读取文件需要十秒种Thread.sleep(2000);}};thread.start(); // 开启t1线程log.debug("t1 state:{}",thread.getState()); // 获取t1线程的状态 Thread.sleep(500); // 主线程阻塞 500毫秒log.debug("t1 state:{}",thread.getState()); // 再次获取t1线程的状态 RUNNABLE}
}
- sleep打断 _interrupt()
package com.jsxs.utils;import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;import java.io.FileReader;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;/*** @Author Jsxs* @Date 2023/9/29 13:20* @PackageName:com.jsxs.utils* @ClassName: Sync* @Description: TODO* @Version 1.0*/@Slf4j(topic = "c.Sync")
public class Sync {public static void main(String[] args) throws Exception{Thread thread = new Thread("t1") {@SneakyThrows@Overridepublic void run() {log.debug("进入睡眠 .... ");try {Thread.sleep(2000);} catch (InterruptedException e) {// 当线程睡眠被打断之后,会报异常....log.debug("wake up ....");e.printStackTrace();}}};thread.start();Thread.sleep(1000); //主线程睡眠 1slog.debug("开始唤醒.....");thread.interrupt(); // 主线程睡眠1s之后,执行interrupt打断t1线程进入 唤醒状态}
}
(3).yield() _(Running->Runnable)
- 调用 yield 会让当前线程从
Running
进入Runnable
就绪状态,然后调度执行其它线程 - 具体的实现依赖于操作系统的任务调度器
(4).线程优先级 和 yield()
- 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
- 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用
- 验证yield() 礼让
正常情况下我们不加优先级和yeild等做干涉,那么count1 和 count 2的最后结果将会相差不大。
package com.jsxs.utils;import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;import java.io.FileReader;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;/*** @Author Jsxs* @Date 2023/9/29 13:20* @PackageName:com.jsxs.utils* @ClassName: Sync* @Description: TODO* @Version 1.0*/@Slf4j(topic = "c.Sync")
public class Sync {public static void main(String[] args) throws Exception {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.start();t2.start();}
}
- 优先级设置
package com.jsxs.utils;import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;import java.io.FileReader;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;/*** @Author Jsxs* @Date 2023/9/29 13:20* @PackageName:com.jsxs.utils* @ClassName: Sync* @Description: TODO* @Version 1.0*/@Slf4j(topic = "c.Sync")
public class Sync {public static void main(String[] args) throws Exception {Runnable task1 = () -> {int count = 0;for (; ; ) { // 死循环System.out.println("---->1 " + count++);}};Runnable task2 = () -> {int count = 0;for (; ; ) {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();}
}
(5).sleep应用_防止CPU占用100%
在没有利用cpu来计算时,不要让while(true)空转浪费cpu,这时可以使用 yield 或 sleep
来让出cpu的使用权给其他程序。
package com.jsxs.utils;import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;import java.io.FileReader;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;/*** @Author Jsxs* @Date 2023/9/29 13:20* @PackageName:com.jsxs.utils* @ClassName: Sync* @Description: TODO* @Version 1.0*/@Slf4j(topic = "c.Sync")
public class Sync {public static void main(String[] args) throws Exception {while (true) {try {Thread.sleep(50); // 加时间进行间隔,减少一直对cpu的占用} catch (InterruptedException e) {e.printStackTrace();}}}
}
在单核cpu下,不加等待时间的cpu占用率达到了98%,如果加上了我们的时间做间隔限制,我们的cpu资源占用率降低到了3%。
- 可以使用 wait 或条件变量达到类似的效果。
- 不同的是,后两种都需要加锁,并且需要相应的唤醒操作,一般适用于要进行同步的场景。
- sleep 使用于无需锁同步的场景。
(6).join_等待线程运行结束
为什么需要join
下面的代码执行,打印r是什么?
package com.jsxs.utils;import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;import java.io.FileReader;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;/*** @Author Jsxs* @Date 2023/9/29 13:20* @PackageName:com.jsxs.utils* @ClassName: Sync* @Description: TODO* @Version 1.0*/@Slf4j(topic = "c.Sync")
public class Sync {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 {Thread.sleep(1); // 这里需要进行睡眠的操作} catch (InterruptedException e) {e.printStackTrace();}log.debug("结束");r = 10;},"t1");t1.start(); // 开启log.debug("结果为:{}", r); // 结果为0,因为t1先睡眠了一会,所以结果为0。log.debug("结束");}
}
结果我们理想化的应该是10,为甚恶会是1呢?
分析
- 因为主线程和线程
t1
是并行执行的,t1 线程需要 1 秒之后才能算出 r=10。 - 而主线程一开始就要打印 r 的结果,
所以只能打印出 r=0
。
解决方法
- 用 sleep 行不行?为什么?(因为不知道子线程需要多长时间结束)
- 用 join,加在 t1.start() 之后即可
package com.jsxs.utils;import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;import java.io.FileReader;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;/*** @Author Jsxs* @Date 2023/9/29 13:20* @PackageName:com.jsxs.utils* @ClassName: Sync* @Description: TODO* @Version 1.0*/@Slf4j(topic = "c.Sync")
public class Sync {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 {Thread.sleep(1); // 这里需要进行睡眠的操作} catch (InterruptedException e) {e.printStackTrace();}log.debug("结束");r = 10;},"t1");t1.start(); // 开启t1.join(); // 同步等待阻塞当这个线程阻塞结束之后,后面的才能运行log.debug("结果为:{}", r); // 因为当阻塞释放后才会运行,所以结果为10log.debug("结束");}
}
(7).join_同步应用
以调用方角度来讲,如果
- 需要等待结果返回,才能继续运行就是同步
- 不需要等待结果返回,就能继续运行就是异步
package com.jsxs.utils;import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;import java.io.FileReader;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;/*** @Author Jsxs* @Date 2023/9/29 13:20* @PackageName:com.jsxs.utils* @ClassName: Sync* @Description: TODO* @Version 1.0*/@Slf4j(topic = "c.Sync")
public class Sync {static int r1 = 0;static int r2 = 0;public static void main(String[] args) throws InterruptedException {test2();}private static void test2() throws InterruptedException {Thread t1 = new Thread(() -> {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}r1 = 10;});Thread t2 = new Thread(() -> {try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}r2 = 20;});long start = System.currentTimeMillis();t1.start();t2.start();t1.join();t2.join();long end = System.currentTimeMillis();log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);}
}
线程1和线程2同时开启线程,首先线程1会停顿1秒,然后线程2会停顿两秒,又因为不是在主线程进行停顿,所以当线程1停顿一秒的时候,线程2也已经停顿一秒了,再只需要等待一秒我们就可以成功了。所以总功用时2秒。
(8).interrupt_(RUNNING->Waitting)
- 打断 sleep 的线程, 会清空打断状态
打断 sleep 的线程, 会清空打断状态,以 sleep 为例.
- Interruped -> Thread的静态方法 (会清空状态)
- Interrupt -> Thread的实列对象方法 (不会清空状态)
package com.jsxs.utils;import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;import java.io.FileReader;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;/*** @Author Jsxs* @Date 2023/9/29 13:20* @PackageName:com.jsxs.utils* @ClassName: Sync* @Description: TODO* @Version 1.0*/@Slf4j(topic = "c.Sync")
public class Sync {public static void main(String[] args) throws InterruptedException {test1();}private static void test1() throws InterruptedException {Thread t1 = new Thread(()->{try {Thread.sleep(1000); //} catch (InterruptedException e) {e.printStackTrace();}}, "t1");t1.start();Thread.sleep(100);t1.interrupt();log.debug(" 打断状态: {}", t1.interrupted()); // interrupted() 会清空打断状态 true变成false}
}
使用interrupted() 打断非运行(Waitting)的线程那么会抛出异常且清空状态!!!;假如使用Interrupt那么会爆出异常但不会清空状态。
- 打断正常运行的线程
打断正常运行的线程, 不会清空打断状态 ( IsInterrupted() 不会清空状态)
package com.jsxs.utils;import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;import java.io.FileReader;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;/*** @Author Jsxs* @Date 2023/9/29 13:20* @PackageName:com.jsxs.utils* @ClassName: Sync* @Description: TODO* @Version 1.0*/@Slf4j(topic = "c.Sync")
public class Sync {public static void main(String[] args) throws InterruptedException {test2();}private static void test2() throws InterruptedException {Thread t2 = new Thread(()->{while(true) {Thread current = Thread.currentThread(); // 获取当前线程boolean interrupted = current.isInterrupted(); //当前线程是否被打的if(interrupted) {log.debug(" 打断状态: {}", interrupted);break; // 我们被打断之后,这个while并不是说不再运行了}}}, "t2");t2.start();Thread.sleep(500);t2.interrupt(); //执行打断 不清空状态}
}
使用Interrupt()打断正常的运行不会触发异常的操作,也不会清空打断状态!!!!;假如使用interrupted()不会出现异常但是会清空打断状态。
(9).interrupt_两阶段终止
在一个线程T1种如何优雅终止线程T2? 这里的优雅指的是给T2一个料理后事的机会。
错误思路
-
使用线程对象的 stop() 方法停止线程
- stop方法真正杀死线程,如果这时线程锁住了共享资源,那么当它被杀死后就再也没有机会释放锁,其他线程将永远无法获取锁。 (死锁)
-
使用System.exit(int) 方法停止线程
- 目的仅是停止一个线程,但这种做法会让整个程序都停止。
- 两阶段终止_interrupt分析
- 两阶段终止_interrupt 异常处理
package com.jsxs.utils;import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;import java.io.FileReader;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;/*** @Author Jsxs* @Date 2023/9/29 13:20* @PackageName:com.jsxs.utils* @ClassName: Sync* @Description: TODO* @Version 1.0*/@Slf4j(topic = "c.Sync")
public class Sync {public static void main(String[] args) throws InterruptedException {TwoPhaseTermination termination = new TwoPhaseTermination();termination.start(); //开启监控Thread.sleep(100); // 休眠termination.stop(); // 停止监控}
}@Slf4j(topic = "c.Sync")
class TwoPhaseTermination{private Thread monitor;// 启动监控线程public void start(){monitor=new Thread(){@Overridepublic void run() {while (true){Thread thread = Thread.currentThread(); //获取当前线程对象if (thread.isInterrupted()) {log.debug("料理后事.....");break;}try {Thread.sleep(10000); //1. 非正常打断,清空运行状态。 比如说:打断状态为真清空后变成假。⭐log.debug("执行监控功能...."); // 2. 正常打断,不清空运行状态⭐⭐} catch (InterruptedException e) {log.debug("被interrupt给打断");currentThread().interrupt(); // 非正常打断的情况下,再次进行打断⭐⭐e.printStackTrace();}}}};monitor.start();}// 停止监控线程public void stop(){monitor.interrupt(); //使用的是 interrupt() 不会清空状态}
}
假如我们使用两阶段终止的情况下,直接打断正常运行(RUNNING)的线程不会清空状态;假如遇到了非正在运行(Waitting)的线程那么会清空状态。
(10).park_打断线程
- 一个 park 在线程中
打断 park 线程, 不会清空打断状态
package com.jsxs.utils;import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;import java.io.FileReader;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.locks.LockSupport;/*** @Author Jsxs* @Date 2023/9/29 13:20* @PackageName:com.jsxs.utils* @ClassName: Sync* @Description: TODO* @Version 1.0*/@Slf4j(topic = "c.Sync")
public class Sync {public static void main(String[] args) throws InterruptedException {test3();}private static void test3() throws InterruptedException {Thread t1 = new Thread(() -> {log.debug("park...");LockSupport.park(); // 进行打断处理log.debug("unpark...");log.debug("打断状态:{}", Thread.currentThread().isInterrupted());}, "t1");t1.start();Thread.sleep(500);t1.interrupt();}
}
- 两个park 在线程中
如果打断标记已经是 true, 则 再调用一个park 会失效(就是变成未被打断所以一直运行)。
package com.jsxs.utils;import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;import java.io.FileReader;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.locks.LockSupport;/*** @Author Jsxs* @Date 2023/9/29 13:20* @PackageName:com.jsxs.utils* @ClassName: Sync* @Description: TODO* @Version 1.0*/@Slf4j(topic = "c.Sync")
public class Sync {public static void main(String[] args) throws InterruptedException {test3();}private static void test3() throws InterruptedException {Thread t1 = new Thread(() -> {log.debug("park...");LockSupport.park(); // 进行打断处理log.debug("unpark...");log.debug("打断状态:{}", Thread.currentThread().interrupted()); // interrupted 返回状态且清空打断状态(true->false;false->true), isInterrupted 返回状态不清空打断状态LockSupport.park(); // 会造成失效也就是 打断+打断=未打断。假如使用interrupted会先清空打断状态,然后再打断。log.debug("unpark...");}, "t1");t1.start();Thread.sleep(500);t1.interrupt();}
}
interrupted 返回状态且清空打断状态(true->false;false->true), isInterrupted 返回状态不清空打断状态。正常情况下会继续打印第二个park()下的打印的;但是我们使用interrupted之后就不会打印了。
(11).不推荐的打断方法
还有一些不推荐使用的方法,这些方法已经过时,容易破坏同步代码块,造成线程死锁。
5.主线程与守护线程
(1).守护线程
默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束
。
package com.jsxs.utils;import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;import java.io.FileReader;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.locks.LockSupport;/*** @Author Jsxs* @Date 2023/9/29 13:20* @PackageName:com.jsxs.utils* @ClassName: Sync* @Description: TODO* @Version 1.0*/@Slf4j(topic = "c.Sync")
public class Sync {public static void main(String[] args) throws Exception {log.debug("开始运行...");Thread t1 = new Thread(() -> {log.debug("开始运行...");try {Thread.sleep(2000); // 守护线程设置两秒} catch (InterruptedException e) {e.printStackTrace();}log.debug("运行结束...");}, "daemon");// 设置该线程为守护线程t1.setDaemon(true);t1.start();Thread.sleep(1000); // 主线程睡眠1slog.debug("运行结束...");}}
注意:
- 垃圾回收器线程就是一种守护线程
- Tomcat 中的
Acceptor
和Poller
线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待它们处理当前请求。
6.线程状态
(1).五种状态_操作系统
- 【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联
- 【可运行状态】指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行
- 【运行状态】指获取了 CPU 时间片运行中的状态
- 当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换
- 【阻塞状态】
- 如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入【阻塞状态】
- 等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
- 与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们
- 【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态
(2).六种状态_Java API
根据 Thread.State 枚举,分为六种状态
NEW
线程刚被创建,但是还没有调用 start() 方法RUNNABLE
当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)BLOCKED
,WAITING
,TIMED_WAITING
都是 Java API 层面对【阻塞状态】的细分,后面会在状态转换一节详述TERMINATED
当线程代码运行结束.
(3).线程六种状态演示
package com.jsxs.utils;import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;import java.io.FileReader;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.locks.LockSupport;/*** @Author Jsxs* @Date 2023/9/29 13:20* @PackageName:com.jsxs.utils* @ClassName: Sync* @Description: TODO* @Version 1.0*/@Slf4j(topic = "c.Sync")
public class Sync {public static void main(String[] args) throws Exception {Thread t1 = new Thread("t1") { // 1. NEW状态@Overridepublic void run() {log.debug("running..");}};// 2.阻塞状态Thread t2 = new Thread("t2") { //2. Runnable (可运行、运行、阻塞)@Overridepublic void run() {while (true) {}}};t2.start();Thread t3 = new Thread("t3") { //3. Runnable (运行状态)@Overridepublic void run() {log.debug("running..");}};t3.start();Thread t4 = new Thread("t4") {@Overridepublic void run() {synchronized (Sync.class){try {Thread.sleep(10000);} catch (InterruptedException e) {e.printStackTrace();}}}};t4.start();Thread t5 = new Thread("t5") {@Overridepublic void run() {try {t2.join();} catch (InterruptedException e) {e.printStackTrace();}}};t5.start();Thread t6 = new Thread("t6") {@Overridepublic void run() {synchronized (Sync.class){try {Thread.sleep(10000);} catch (InterruptedException e) {e.printStackTrace();}}}};t6.start();try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}log.debug("t1 state : {}",t1.getState()); //打印NEW,因为没有启动线程log.debug("t2 state : {}",t2.getState()); //打印Runnable,因为处于运行log.debug("t3 state : {}",t3.getState()); //打印Terminate,因为主线程先比t3线程先执行完毕log.debug("t4 state : {}",t4.getState()); //打印TIMED_WAITING,因为有时限的在休眠log.debug("t5 state : {}",t5.getState()); //打印WAITING,因为一直在等待log.debug("t6 state : {}",t6.getState()); //打印BLOCKED,因为获取不到锁资源,t4一直在占用}}
7.统筹规划_分析
(1).问题定义
阅读华罗庚《统筹方法》,给出烧水泡茶的多线程解决方案,提示
- 参考图二,用两个线程(两个人协作)模拟烧水泡茶过程
- 文中办法乙、丙都相当于任务串行
- 而图一相当于启动了 4 个线程,有点浪费
- 用 sleep(n) 模拟洗茶壶、洗水壶等耗费的时间
附:华罗庚《统筹方法》
统筹方法,是一种安排工作进程的数学方法。它的实用范围极广泛,在企业管理和基本建设中,以及关系复杂的科研项目的组织与管理中,都可以应用。
怎样应用呢?主要是把工序安排好。
比如,想泡壶茶喝。当时的情况是:开水没有;水壶要洗,茶壶、茶杯要洗;火已生了,茶叶也有了。怎么办?
- 办法甲:洗好水壶,灌上凉水,放在火上;在等待水开的时间里,洗茶壶、洗茶杯、拿茶叶;等水开了,泡茶喝。
- 办法乙:先做好一些准备工作,洗水壶,洗茶壶茶杯,拿茶叶;一切就绪,灌水烧水;坐待水开了,泡茶喝。
- 办法丙:洗净水壶,灌上凉水,放在火上,坐待水开;水开了之后,急急忙忙找茶叶,洗茶壶茶杯,泡茶喝。
哪一种办法省时间?我们能一眼看出,第一种办法好,后两种办法都窝了工。
这是小事,但这是引子,可以引出生产管理等方面有用的方法来。
水壶不洗,不能烧开水,因而洗水壶是烧开水的前提。没开水、没茶叶、不洗茶壶茶杯,就不能泡茶,因而这些又是泡茶的前提。它们的相互关系,可以用下边的箭头图来表示:
从这个图上可以一眼看出,办法甲总共要16分钟(而办法乙、丙需要20分钟)。如果要缩短工时、提高工作效率,应当主要抓烧开水这个环节,而不是抓拿茶叶等环节。同时,洗茶壶茶杯、拿茶叶总共不过4分钟,大可利用“等水开”的时间来做。
是的,这好像是废话,卑之无甚高论。有如走路要用两条腿走,吃饭要一口一口吃,这些道理谁都懂得。但稍有变化,临事而迷的情况,常常是存在的。在近代工业的错综复杂的工艺过程中,往往就不是像泡茶喝这么简单了。任务多了,几百几千,甚至有好几万个任务。关系多了,错综复杂,千头万绪,往往出现“万事俱备,只欠东风”的情况。由于一两个零件没完成,耽误了一台复杂机器的出厂时间。或往往因为抓的不是关键,连夜三班,急急忙忙,完成这一环节之后,还得等待旁的环节才能装配。
洗茶壶,洗茶杯,拿茶叶,或先或后,关系不大,而且同是一个人的活儿,因而可以合并成为:
看来这是“小题大做”,但在工作环节太多的时候,这样做就非常必要了。
这里讲的主要是时间方面的事,但在具体生产实践中,还有其他方面的许多事。这种方法虽然不一定能直接解决所有问题,但是,我们利用这种方法来考虑问题,也是不无裨益的。
(2).代码展示
package com.jsxs.utils;import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;import java.io.FileReader;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.locks.LockSupport;/*** @Author Jsxs* @Date 2023/9/29 13:20* @PackageName:com.jsxs.utils* @ClassName: Sync* @Description: TODO* @Version 1.0*/@Slf4j(topic = "c.Sync")
public class Sync {public static void main(String[] args) throws Exception {Thread t1 = new Thread("user1") {@Overridepublic void run() {log.debug("洗水壶.... 2s");try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}log.debug("烧水壶...3s");try {Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();}}};t1.start();new Thread("user2"){@Overridepublic void run() {log.debug("洗茶壶.... 1s");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}log.debug("洗茶杯.... 2s");try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}log.debug("拿茶叶.... 1s");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}try {t1.join(); // 等待t1线程结束之后,进行泡茶log.debug("user2 泡茶");} catch (InterruptedException e) {e.printStackTrace();}}}.start();}}
最佳耗时 7s
(四)、共享模型之管程
1.共享问题
(1).小故事
老王(操作系统)有一个功能强大的算盘(CPU),现在想把它租出去,赚一点外快
- 小南、小女(线程)来使用这个算盘来进行一些计算,并按照时间给老王支付费用
- 但小南不能一天24小时使用算盘,他经常要小憩一会(sleep),又或是去吃饭上厕所(阻塞 io 操作),有时还需要一根烟,没烟时思路全无(wait)这些情况统称为(阻塞)
- 在这些时候,算盘没利用起来(不能收钱了),老王觉得有点不划算
- 另外,小女也想用用算盘,如果总是小南占着算盘,让小女觉得不公平
- 于是,老王灵机一动,想了个办法 [ 让他们每人用一会,轮流使用算盘 ]
- 这样,当小南阻塞的时候,算盘可以分给小女使用,不会浪费,反之亦然
- 最近执行的计算比较复杂,需要存储一些中间结果,而学生们的脑容量(工作内存)不够,所以老王申请了一个笔记本(主存),把一些中间结果先记在本上
- 计算流程是这样的
- 但是由于分时系统,有一天还是发生了事故
- 小南刚读取了初始值 0 做了个 +1 运算,还没来得及写回结果
- 老王说 [ 小南,你的时间到了,该别人了,记住结果走吧 ],于是小南念叨着 [ 结果是1,结果是1…] 不甘心地到一边待着去了(上下文切换)
- 老王说 [ 小女,该你了 ],小女看到了笔记本上还写着 0 做了一个 -1 运算,将结果 -1 写入笔记本
- 这时小女的时间也用完了,老王又叫醒了小南:[小南,把你上次的题目算完吧],小南将他脑海中的结果 1 写入了笔记本
- 小南和小女都觉得自己没做错,但笔记本里的结果是 1 而不是 0
(2).Java 的体现
两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗?
package com.jsxs.utils;import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;import java.io.FileReader;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.locks.LockSupport;/*** @Author Jsxs* @Date 2023/9/29 13:20* @PackageName:com.jsxs.utils* @ClassName: Sync* @Description: TODO* @Version 1.0*/@Slf4j(topic = "c.Sync")
public class Sync {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);}}
(3).问题分析
以上的结果可能是正数、负数、零
。为什么呢?因为 Java 中对静态变量的自增,自减并不是原子操作,要彻底理解,必须从字节码来进行分析。
例如对于 i++
而言(i 为静态变量),实际会产生如下的 JVM 字节码指令:
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 自增
putstatic i // 将修改后的值存入静态变量i
而对应 i--
也是类似:
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
isub // 自减
putstatic i // 将修改后的值存入静态变量i
而 Java 的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换:
如果是单线程以上 8 行代码是顺序执行(不会交错)没有问题:
但多线程下这 8 行代码可能交错运行:
出现负数的情况
首先线程2获取静态变量并减1,这时候还没写入主存呢,然后遇到了上下文的切换,线程1读取的数据是0,然后赋值为1并写入主存。这时候线程一的时间片用完了,轮到线程2进行操作了。继续接着执行线程2写入主存的操作,也就是说会将原本主存中的1覆盖为-1.
出现正数的情况: (线程1会覆盖线程2)
线程一先读取数据,然后才线程二进行读取
(4).临界区 Critical Section
- 一个程序运行多个线程本身是没有问题的
- 问题出在多个线程访问共享资源
- 多个线程读共享资源其实也没有问题
- 在多个线程对共享资源读写操作时发生指令交错,就会出现问题
- 一段代码块内
如果存在对共享资源的多线程读写操作
,称这段代码块为临界区
static int counter = 0;static void increment()
// 临界区
{ counter++;
}static void decrement()
// 临界区
{ counter--;
}
(5).竞态条件 Race Condition
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
2. synchronized 解决方案
(1).应用之互斥
为了避免临界区的竞态条件发生,有多种手段可以达到目的。
- 阻塞式的解决方案:synchronized,Lock
- 非阻塞式的解决方案:原子变量
本次课使用阻塞式的解决方案:synchronized
,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住
。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换。
注意:
虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:
- 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
- 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点
(2).synchronized 对象锁
synchronized: 释放资源但不释放锁!!!
语法
synchronized(对象) // 线程1, 线程2(blocked)
{临界区
}
解决
package com.jsxs.utils;import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;import java.io.FileReader;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.locks.LockSupport;/*** @Author Jsxs* @Date 2023/9/29 13:20* @PackageName:com.jsxs.utils* @ClassName: Sync* @Description: TODO* @Version 1.0*/@Slf4j(topic = "c.Sync")
public class Sync {static int counter = 0;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) { // 对象锁: 假如不释放锁,那么其他对象锁 ⭐counter++;}}}, "t1");Thread t2 = new Thread(() -> {for (int i = 0; i < 5000; i++) {synchronized (room) { // 对象锁: 假如不释放锁,那么其他对象锁 ⭐counter--;}}}, "t2");t1.start();t2.start();t1.join();t2.join();log.debug("{}", counter);}}
(3).synchrpnized_理解
- 理论概述
- synchronized(
对象
) 中的对象,可以想象为一个房间(room),有唯一入口(门)房间只能一次进入一人进行计算,线程 t1,t2 想象成两个人 - 当线程 t1 执行到 synchronized(room) 时就好比
t1 进入了这个房间,并锁住了门拿走了钥匙
,在门内执行count++ 代码 - 这时候如果
t2 也运行到了 synchronized(room) 时,它发现门被锁住了,只能在门外等待,发生了上下文切换,阻塞住了
- 这中间即使 t1 的 cpu 时间片不幸用完,被踢出了门外(不要错误理解为锁住了对象就能一直执行下去哦),
这时门还是锁住的,t1 仍拿着钥匙,t2 线程还在阻塞状态进不来,只有下次轮到 t1 自己再次获得时间片时才能开门进入
- 当
t1 执行完 synchronized{} 块内的代码,这时候才会从 obj 房间出来并解开门上的锁,唤醒 t2 线程把钥匙给他
。t2 线程这时才可以进入 obj 房间,锁住了门拿上钥匙,执行它的 count-- 代码.
注意: 当临界区的代码执行完毕之后才会释放锁。
- 用图来理解
(4).synchorized_思考
synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断。
为了加深理解,请思考下面的问题
- 如果 t1 synchronized(obj1) 且 t2 synchronized(obj1)会怎么运作? – 原子性✅
- 如果 t1 synchronized(obj1) 而 t2 synchronized(obj2) 会怎样运作?– 锁对象
- 如果 t1 synchronized(obj) 而 t2 没有加会怎么样?如何理解?– 锁对象
3.synchorized_面向对象改进
(1).面向对象改进
把需要保护的共享变量放入一个类
package com.jsxs.utils;import lombok.extern.slf4j.Slf4j;/*** @Author Jsxs* @Date 2023/9/29 13:20* @PackageName:com.jsxs.utils* @ClassName: Sync* @Description: TODO* @Version 1.0*/@Slf4j(topic = "c.Sync")
public class Sync {public static void main(String[] args) throws InterruptedException {Room room = new Room();Thread t1 = new Thread(() -> {for (int j = 0; j < 5000; j++) {room.increment();}}, "t1");Thread t2 = new Thread(() -> {for (int j = 0; j < 5000; j++) {room.decrement();}}, "t2");t1.start();t2.start();t1.join();t2.join();log.debug("count: {}", room.get());}static class Room {int value = 0;public void increment() {synchronized (this) { // 改进锁对象,锁的是同一个类value++;}}public void decrement() {synchronized (this) { //改进锁对象,锁的是同一个类value--;}}public int get() {synchronized (this) {return value;}}}}
4.synchorized_作用域
(1).添加在非静态方法上 _ 锁住的是this
添加成员方法上,相当于锁的是this对象
class Test{public synchronized void test() {}
}
等价于
class Test{public void test() {synchronized(this) {}}
}
(2).添加在静态方法上 _ 锁住的是类对象
添加在静态方法上,相当于锁住的是类对象
class Test{public synchronized static void test() {}
}等价于class Test{public static void test() {synchronized(Test.class) {}}
}
(3).不加 synchronized 的方法
不加 synchronzied
的方法就好比不遵守规则的人,不去老实排队(好比翻窗户进去的)。没有办法保证他的原子性的。
5. 所谓的"线程八锁"
起始就是考察 synchorized 锁住的是哪个对象。
(1).情况1_ 两个都没有睡眠
package com.jsxs.utils;import lombok.extern.slf4j.Slf4j;/*** @Author Jsxs* @Date 2023/9/29 13:20* @PackageName:com.jsxs.utils* @ClassName: Sync* @Description: TODO* @Version 1.0*/@Slf4j(topic = "c.Sync")
public class Sync {public static void main(String[] args) throws InterruptedException {Number n1 = new Number();new Thread(() -> {n1.a();}).start();new Thread(() -> {n1.b();}).start();}static class Number {public synchronized void a() { // 锁的是: Number对象⭐log.debug("1");}public synchronized void b() { // 锁的是: Number对象⭐log.debug("2");}}}
顺序输出: 1 2 或 2 1
(2).情况2_其中一个有睡眠
package com.jsxs.utils;import lombok.extern.slf4j.Slf4j;/*** @Author Jsxs* @Date 2023/9/29 13:20* @PackageName:com.jsxs.utils* @ClassName: Sync* @Description: TODO* @Version 1.0*/@Slf4j(topic = "c.Sync")
public class Sync {public static void main(String[] args) throws InterruptedException {Number n1 = new Number();new Thread(() -> {n1.a();}).start();new Thread(() -> {n1.b();}).start();}static class Number {public synchronized void a() { // 锁的是: Number对象try {Thread.sleep(1000); // ⭐} catch (InterruptedException e) {e.printStackTrace();}log.debug("1");}public synchronized void b() { // 锁的是: Number对象log.debug("2");}}}
①: 1秒后输出 1 2 。 ②: 0秒输出2然后1s后输出1
(3).情况3_存在一个没有加锁的方法
package com.jsxs.utils;import lombok.extern.slf4j.Slf4j;/*** @Author Jsxs* @Date 2023/9/29 13:20* @PackageName:com.jsxs.utils* @ClassName: Sync* @Description: TODO* @Version 1.0*/@Slf4j(topic = "c.Sync")
public class Sync {public static void main(String[] args) throws InterruptedException {Number n1 = new Number();new Thread(() -> {n1.a();}).start();new Thread(() -> {n1.b();}).start();new Thread(() -> {n1.c();}).start();}static class Number {public synchronized void a() { // 锁的是: Number对象try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}log.debug("1");}public synchronized void b() { // 锁的是: Number对象log.debug("2");}public void c() { // 未加锁 ⭐log.debug("3");}}}
输出结果: ①.0秒后输出3,然后1秒后输出1 2。②.0秒后输出3和2然后1秒后输出0
(4).情况4_锁同类两个不同对象
不存在互斥的情况....
package com.jsxs.utils;import lombok.extern.slf4j.Slf4j;/*** @Author Jsxs* @Date 2023/9/29 13:20* @PackageName:com.jsxs.utils* @ClassName: Sync* @Description: TODO* @Version 1.0*/@Slf4j(topic = "c.Sync")
public class Sync {public static void main(String[] args) throws InterruptedException {Number n1 = new Number(); // 锁的是 n1 ⭐Number n2 = new Number(); // 所得是 n2 ⭐new Thread(() -> {n1.a();}).start();new Thread(() -> {n2.b();}).start();}static class Number {public synchronized void a() { // 锁的是: Number对象try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}log.debug("1");}public synchronized void b() { // 锁的是: Number对象log.debug("2");}}}
一定是先输出2 然后1秒后输出1
(5).情况5_锁一个静态方法
锁静态方法相当于锁住的是整个类
类锁和对象锁不互斥
package com.jsxs.utils;import lombok.extern.slf4j.Slf4j;/*** @Author Jsxs* @Date 2023/9/29 13:20* @PackageName:com.jsxs.utils* @ClassName: Sync* @Description: TODO* @Version 1.0*/@Slf4j(topic = "c.Sync")
public class Sync {public static void main(String[] args) throws InterruptedException {Number n1 = new Number();new Thread(() -> {n1.a();}).start();new Thread(() -> {n1.b();}).start();}static class Number {public static synchronized void a() { // 锁的是: Number对象 ⭐try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}log.debug("1");}public synchronized void b() { // 锁的是: Number对象log.debug("2");}}}
顺序一定是先2然后1秒后1
(6).情况6_锁两个静态方法
package com.jsxs.utils;import lombok.extern.slf4j.Slf4j;/*** @Author Jsxs* @Date 2023/9/29 13:20* @PackageName:com.jsxs.utils* @ClassName: Sync* @Description: TODO* @Version 1.0*/@Slf4j(topic = "c.Sync")
public class Sync {public static void main(String[] args) throws InterruptedException {Number n1 = new Number();new Thread(() -> {n1.a();}).start();new Thread(() -> {n1.b();}).start();}static class Number {public static synchronized void a() { // 锁的是: Number对象 ⭐try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}log.debug("1");}public static synchronized void b() { // 锁的是: Number对象 ⭐log.debug("2");}}}
执行顺序: ①一秒后1然后2。②0秒后2然后1秒后1
(7).情况7_锁同类两个不同对象+锁一个静态方法
package com.jsxs.utils;import lombok.extern.slf4j.Slf4j;/*** @Author Jsxs* @Date 2023/9/29 13:20* @PackageName:com.jsxs.utils* @ClassName: Sync* @Description: TODO* @Version 1.0*/@Slf4j(topic = "c.Sync")
public class Sync {public static void main(String[] args) throws InterruptedException {Number n1 = new Number();Number n2 = new Number();new Thread(() -> {n1.a();}).start();new Thread(() -> {n2.b();}).start();}static class Number {public static synchronized void a() { // 锁的是: Number对象try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}log.debug("1");}public synchronized void b() { // 锁的是: Number对象log.debug("2");}}
}
一定先是 先2然后1秒后1
(8).情况8_锁同类两个不同对象+锁两个静态方法
两个类锁,所以互斥
package com.jsxs.utils;import lombok.extern.slf4j.Slf4j;/*** @Author Jsxs* @Date 2023/9/29 13:20* @PackageName:com.jsxs.utils* @ClassName: Sync* @Description: TODO* @Version 1.0*/@Slf4j(topic = "c.Sync")
public class Sync {public static void main(String[] args) throws InterruptedException {Number n1 = new Number();Number n2 = new Number();new Thread(() -> {n1.a();}).start();new Thread(() -> {n2.b();}).start();}static class Number {public static synchronized void a() { // 锁的是: Number对象try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}log.debug("1");}public static synchronized void b() { // 锁的是: Number对象log.debug("2");}}}
顺序是: ①1秒后1然后2;②0秒后2然后一秒后1
6.线程安全分析
无状态就是: 无成员变量或有成员变量(但只读)
(1).成员变量和静态变量是否线程安全?
- 如果它们没有共享,则线程安全。
- 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况。
- 如果只有读操作,则线程安全。
- 如果有读写操作,则这段代码是临界区,需要考虑线程安全。
(2).局部变量是否线程安全?
- 局部变量是线程安全的
- 但局部变量引用的对象则未必
- 如果该对象没有逃离方法的作用访问,它是线程安全的。
- 如果该对象逃离方法的作用范围,需要考虑线程安全。
(3).局部变量线程安全分析
- 局部变量引用的是一个基本类型
public static void test1() {int i = 10;i++;}
每个线程调用 test1() 方法时局部变量 i,会在每个线程的栈帧内存中被创建多份,因此不存在共享。
public static void test1();descriptor: ()Vflags: ACC_PUBLIC, ACC_STATICCode:stack=1, locals=1, args_size=00: bipush 10 // 赋值为102: istore_03: iinc 0, 1 // 做自增16: return // 返回结果LineNumberTable:line 10: 0line 11: 3line 12: 6LocalVariableTable:Start Length Slot Name Signature3 4 0 i I
- 局部变量的引用_堆中同一个实列 (成员变量)
package com.jsxs.utils;import lombok.extern.slf4j.Slf4j;import java.util.ArrayList;/*** @Author Jsxs* @Date 2023/9/29 13:20* @PackageName:com.jsxs.utils* @ClassName: Sync* @Description: TODO* @Version 1.0*/@Slf4j(topic = "c.Sync")
public class Sync {static final int THREAD_NUMBER = 2;static final int LOOP_NUMBER = 200;public static void main(String[] args) throws InterruptedException {ThreadUnsafe test = new ThreadUnsafe();for (int i = 0; i < THREAD_NUMBER; i++) {new Thread(() -> {test.method1(LOOP_NUMBER);}, "Thread" + i).start();}}
}class ThreadUnsafe {ArrayList<String> 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);}
}
其中一种情况是,如果线程2 还未 add,线程1 remove 就会报错:
分析:
- 无论哪个线程中的 method2 引用的都是同一个对象中的 list成员变量
- method3 与 method2 分析相同
- 局部变量的引用_堆中不同实列
引用堆中各自的引用对象。而不是共享的
package com.jsxs.utils;import lombok.extern.slf4j.Slf4j;import java.util.ArrayList;/*** @Author Jsxs* @Date 2023/9/29 13:20* @PackageName:com.jsxs.utils* @ClassName: Sync* @Description: TODO* @Version 1.0*/@Slf4j(topic = "c.Sync")
public class Sync {static final int THREAD_NUMBER = 2;static final int LOOP_NUMBER = 200;public static void main(String[] args) throws InterruptedException {ThreadSafe test = new ThreadSafe();for (int i = 0; i < THREAD_NUMBER; i++) {new Thread(() -> {test.method1(LOOP_NUMBER);}, "Thread" + i).start();}}
}
class ThreadSafe {public final void method1(int loopNumber) {ArrayList<String> list = new ArrayList<>();for (int i = 0; i < loopNumber; i++) {method2(list); // ⭐method3(list); // ⭐}}private void method2(ArrayList<String> list) { // ⭐list.add("1");}private void method3(ArrayList<String> list) { // ⭐list.remove(0);}
}
那么就不会有上述问题了
分析:
- list 是局部变量,每个线程调用时会创建其不同实例,没有共享
- 而 method2 的参数是从 method1 中传递过来的,与 method中引用同一个对象
- method3 的参数分析与 method2 相同
- 方法访问修饰符带来的思考
方法访问修饰符带来的思考,如果把 method2 和 method3 的方法修改为 public 会不会代理线程安全问题?
- 情况1:有其它线程调用 method2 和 method3。 (不会有线程安全问题,因为会新建一个list)
package com.jsxs.utils;import lombok.extern.slf4j.Slf4j;import java.util.ArrayList;/*** @Author Jsxs* @Date 2023/9/29 13:20* @PackageName:com.jsxs.utils* @ClassName: Sync* @Description: TODO* @Version 1.0*/@Slf4j(topic = "c.Sync")
public class Sync {static final int THREAD_NUMBER = 2;static final int LOOP_NUMBER = 200;public static void main(String[] args) throws InterruptedException {ThreadSafe test = new ThreadSafe();for (int i = 0; i < THREAD_NUMBER; i++) {new Thread(() -> {test.method1(LOOP_NUMBER);}, "Thread" + i).start();}}
}
class ThreadSafe {public final void method1(int loopNumber) {ArrayList<String> list = new ArrayList<>();for (int i = 0; i < loopNumber; i++) {method2(list); //用的是同一个局部变量method3(list); //}}public void method2(ArrayList<String> list) { // ⭐list.add("1");}public void method3(ArrayList<String> list) { //⭐list.remove(0);}
}
- 情况2:在 情况1 的基础上,为 ThreadSafe 类添加子类,子类覆盖 method2 或 method3 方法,即
package com.jsxs.utils;import lombok.extern.slf4j.Slf4j;import java.util.ArrayList;/*** @Author Jsxs* @Date 2023/9/29 13:20* @PackageName:com.jsxs.utils* @ClassName: Sync* @Description: TODO* @Version 1.0*/@Slf4j(topic = "c.Sync")
public class Sync {static final int THREAD_NUMBER = 2;static final int LOOP_NUMBER = 200;public static void main(String[] args) throws InterruptedException {ThreadSafeSubClass test = new ThreadSafeSubClass();for (int i = 0; i < THREAD_NUMBER; i++) {new Thread(() -> {test.method1(LOOP_NUMBER);}, "Thread" + i).start();}}
}class ThreadSafe {public final void method1(int loopNumber) {ArrayList<String> list = new ArrayList<>();for (int i = 0; i < loopNumber; i++) {method2(list); method3(list); }}public void method2(ArrayList<String> list) {list.add("1");}public void method3(ArrayList<String> list) {list.remove(0);}
}class ThreadSafeSubClass extends ThreadSafe {@Overridepublic void method3(ArrayList<String> list) {new Thread(() -> {list.remove(0); // 新建一个线程之后,会出现线程安全。因为原本单线程共享一个资源,现在是多线程会出现竞态}).start();}
}
从这个例子可以看出 private
或 final
提供【安全】的意义所在,请体会开闭原则中的【闭】。因为被 private 修饰的方法不能被子类继承,所以不会出现重写method3的情况了。假如子类写了同名的method3,根据双亲委派机制也不会执行子类的method3
(4).常见线程安全类
- String (底层是final)
- Integer (底层是final)
- StringBuffer (底层synchorized)
- Random
- Vector (底层synchorized)
- Hashtable (底层synchorized)
- java.util.concurrent 包下的类
这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为
Hashtable table = new Hashtable();new Thread(()->{table.put("key", "value1");
}).start();new Thread(()->{table.put("key", "value2");
}).start();
- 它们的每个方法是原子的
- 但注意它们多个方法的组合不是原子的,见后面分析
(4.1). 线程安全类方法的组合
分析下面代码是否线程安全?
Hashtable table = new Hashtable();
// 线程1,线程2
if( table.get("key") == null) {table.put("key", value);
}
squenceDiagram
participant t1 as 线程1
participant t1 as 线程2
participant table
t1 ->> table : get("key")==null
t2 ->> table : get("key")==null
t2 ->> table : put("key",v2)
t1 ->> table : put("key",v1)
#这是后会产生覆盖,也就是线程安全问题。v1覆盖v2
(4.2).不可变类线程安全性
String、Integer 等都是不可变类,因为其内部的状态(属性)不可以改变,因此它们的方法都是线程安全的,有同学或许有疑问,String 有 replace,substring
等方法【可以】改变值啊。实质上是NEW了一个新的对象,那么这些方法又是如何保证线程安全的呢?
replace和substring实质上就是NEW新建了一个对象。
(5).实例分析
- 例1: Controller层
package com.jsxs.utils;import lombok.extern.slf4j.Slf4j;import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;/*** @Author Jsxs* @Date 2023/9/29 13:20* @PackageName:com.jsxs.utils* @ClassName: Sync* @Description: TODO* @Version 1.0*/@Slf4j(topic = "c.Sync")
public class MyServlet extends HttpServlet {// 是否安全? -> 不是 (因为HashMap不是线程安全的类)Map<String, Object> map = new HashMap<>();// 是否安全? -> 是 (因为是线程安全类)String S1 = "...";// 是否安全? -> 是 (因为是线程安全类) (被final修饰的引用变量。引用值(S1)不能改变 S2=S2.replace('g','1')->报错; 但是值可以改变 S2.replace('g','1') ->不报错)final String S2 = "..."; // 是否安全? -> 不是 (因为不是线程安全类)Date D1 = new Date();// 是否安全? -> 不是 (因为不是线程安全类) (被final修饰的引用变量。引用值(d2)不能改变,但是值可以变)final Date D2 = new Date();public void doGet(HttpServletRequest request, HttpServletResponse response) {// 调用上面的方法}
}
例2: Serverimpl层
public class MyServlet extends HttpServlet {// 是否安全? ->不是线程安全因为server层存在不安全 ⭐⭐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++;}
}
- 列三: 切面的时候
@Aspect
@Component
public class MyAspect {// 是否安全? 不安全,因为Spring默认切面是单列的,然后里面的成员变量就会被共享。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));}
}
- 列四 : MVC ✅
public class MyServlet extends HttpServlet {// 是否安全 -是安全的因为private UserDao userDao = new UserDaoImpl();安全的 ⭐⭐⭐private UserService userService = new UserServiceImpl();public void doGet(HttpServletRequest request, HttpServletResponse response) {userService.update(...);}
}public class UserServiceImpl implements UserService {// 是否安全 -虽然是成员变量,但是因为底部的Conn是安全的, ⭐⭐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) {// ...}}
}
- 列5: MVC
public class MyServlet extends HttpServlet {// 是否安全: 不安全因为server层private UserService userService = new UserServiceImpl();public void doGet(HttpServletRequest request, HttpServletResponse response) {userService.update(...);}
}public class UserServiceImpl implements UserService {// 是否安全: 不安全因为dao层private UserDao userDao = new UserDaoImpl();public void update() {userDao.update();}}public class UserDaoImpl implements UserDao {// 是否安全 : 不安全成员变量共享了private Connection conn = null;public void update() throws SQLException {String sql = "update user set password = ? where username = ?";conn = DriverManager.getConnection("", "", "");// ...conn.close();}
}
- 列六 MVC✅
public class MyServlet extends HttpServlet {// 是否安全 : 不安全因为server层消除了安全隐患 ⭐private UserService userService = new UserServiceImpl();public void doGet(HttpServletRequest request, HttpServletResponse response) {userService.update(...);}
}public class UserServiceImpl implements UserService {public void update() {// 局部变量不存在线程安全,因为局部化了 ⭐UserDao userDao = new UserDaoImpl();userDao.update();}
}public class UserDaoImpl implements UserDao {// 是否安全? 这里会出现安全问题,但是经过server层就不会了 ⭐private Connection =null;public void update() throws SQLException {String sql = "update user set password = ? where username = ?";conn = DriverManager.getConnection("", "", "");// ...conn.close();}
}
列7 : 外形方法
public abstract class Test {public void bar() {// 是否安全 : 不能确定是否安全,看fooSimpleDateFormat 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 的行为是不确定的,可能导致不安全的发生,被称之为外星方法
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 类的实现。
为什么String被设置成final,主要是为了避免外星方法。保证线程安全。
7.习题
(1).卖票练习
测试下面代码是否存在线程安全问题,并尝试改正。
package com.jsxs.utils;import lombok.extern.slf4j.Slf4j;import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.Vector;/*** @Author Jsxs* @Date 2023/10/2 16:17* @PackageName:com.jsxs.utils* @ClassName: ExerciseSell* @Description: TODO* @Version 1.0*/
@Slf4j(topic = "c.sell")
public class ExerciseSell {public static void main(String[] args) throws InterruptedException {TicketWindow ticketWindow = new TicketWindow(1000); //初始化为1000张票List<Integer> list = new Vector<>(); // 统计所有已经卖出的票List<Thread> threads = new ArrayList<>(); // 统计所有的线程集合for (int i = 0; i < 5000; i++) { // 模拟循环2000个人去抢票Thread thread = new Thread("第" + i + "个顾客") {@Overridepublic void run() {// 顾客买票 : 买票的数目为 0~5int amount = ticketWindow.sell(ExerciseSell.randomAmount());try {Thread.sleep(randomAmount()); // 放大事故发生的概率} catch (InterruptedException e) {e.printStackTrace();}list.add(amount);}};threads.add(thread);thread.start();}// 统计票数之前,先等待线程结束for (Thread thread : threads) {thread.join();}// 统计卖出的票数和剩余的票数log.debug("余票: {}",ticketWindow.getCount());log.debug("卖出去的票数: {}",list.stream().mapToInt(i->i).sum());}// 随机 1~5 张static Random random = new Random();public static int randomAmount(){return random.nextInt(5)+1;}}// 售票窗口
class TicketWindow {private int count;public TicketWindow(int count) {this.count = count;}// 获取余票数量public int getCount() {return count;}// 售票操作public int sell(int amount) {if (this.count >= amount) {this.count -= amount;return amount;} else {return 0;}}
}
另外,用下面的代码行不行,为什么?
List<Integer> sellCount = new ArrayList<>();
(2).卖票解题_受保护的是一个实列对象
需要在临界区上加锁,就能够解决!
package com.jsxs.utils;import lombok.extern.slf4j.Slf4j;import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.Vector;/*** @Author Jsxs* @Date 2023/10/2 16:17* @PackageName:com.jsxs.utils* @ClassName: ExerciseSell* @Description: TODO* @Version 1.0*/
@Slf4j(topic = "c.sell")
public class ExerciseSell {public static void main(String[] args) throws InterruptedException {TicketWindow ticketWindow = new TicketWindow(1000); //初始化为1000张票List<Integer> list = new Vector<>(); // 统计所有已经卖出的票List<Thread> threads = new ArrayList<>(); // 统计所有的线程集合for (int i = 0; i < 5000; i++) { // 模拟循环2000个人去抢票Thread thread = new Thread("第" + i + "个顾客") {@Overridepublic void run() {// 顾客买票 : 买票的数目为 0~5int amount = ticketWindow.sell(ExerciseSell.randomAmount());try {Thread.sleep(randomAmount());} catch (InterruptedException e) {e.printStackTrace();}list.add(amount);}};threads.add(thread);thread.start();}// 统计票数之前,先等待线程结束for (Thread thread : threads) {thread.join();}// 统计卖出的票数和剩余的票数log.debug("余票: {}",ticketWindow.getCount());log.debug("卖出去的票数: {}",list.stream().mapToInt(i->i).sum());}// 随机 1~5 张static Random random = new Random();public static int randomAmount(){return random.nextInt(5)+1;}}// 售票窗口
class TicketWindow {private int count; // 没有添加static,却也是共享变量因为有 get 或 set 对成员变量public TicketWindow(int count) {this.count = count;}// 获取余票数量public int getCount() {return count;}// 售票操作 : 临界区public synchronized int sell(int amount) { // 对我们的临界区进行加锁的操作if (this.count >= amount) {this.count -= amount;return amount;} else {return 0;}}
}
(3).转账练习_受保护的是多个实列对象
package com.jsxs.utils;import lombok.extern.slf4j.Slf4j;import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.Vector;/*** @Author Jsxs* @Date 2023/10/2 16:17* @PackageName:com.jsxs.utils* @ClassName: ExerciseSell* @Description: TODO* @Version 1.0*/
@Slf4j(topic = "c.sell")
public class ExerciseTransfer {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~100public 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) {synchronized (this){ if (this.money > amount) {this.setMoney(this.getMoney() - amount); //这里有两个受保护的对象target.setMoney(target.getMoney() + amount); // 这里有两个受保护的对象}}}
}
因为是要保护多个实列对象,所以我们需要对类做加锁
// 转账操作 : 临界区public void transfer(Account target, int amount) {synchronized (Account.class){ // 因为有多个受保护的对象,不是一个所以需要对整个类加锁if (this.money > amount) {this.setMoney(this.getMoney() - amount); //这里有两个受保护的对象target.setMoney(target.getMoney() + amount); // 这里有两个受保护的对象}}}
8. Monitor 概念
(1).Java 对象头
以32位虚拟机为例
普通对象
|--------------------------------------------------------------|
| Object Header (64 bits) |
|------------------------------------|-------------------------|
| Mark Word (32 bits) | Klass Word (32 bits) |
|------------------------------------|-------------------------|
数组对象
|---------------------------------------------------------------------------------|
| Object Header (96 bits) |
|--------------------------------|-----------------------|------------------------|
| Mark Word(32bits) | Klass Word(32bits) | array length(32bits) |
|--------------------------------|-----------------------|------------------------|
其中 Mark Word 结构为
|-------------------------------------------------------|--------------------|
| Mark Word (32 bits) | State |
|-------------------------------------------------------|--------------------|
| hashcode:25 | age:4 | biased_lock:0 (没有启用偏向锁 🔰) | 01 | Normal (正常状态🔰) |
|-------------------------------------------------------|--------------------|
| thread:23 (线程id) | epoch:2 | age:4 | biased_lock:1 (启用偏向锁 🔰) | 01 | Biased (偏向锁状态) |
|-------------------------------------------------------|--------------------|
| ptr_to_lock_record:30 | 00 | Lightweight Locked (轻量级锁🔰)|
|-------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:30 | 10 | Heavyweight Locked (重量级锁🔰) |
|-------------------------------------------------------|--------------------|
| | 11 | Marked for GC |
|-------------------------------------------------------|--------------------|
64 位虚拟机 Mark Word
|--------------------------------------------------------------------|--------------------|
| Mark Word (64 bits) | State |
|--------------------------------------------------------------------|--------------------|
| unused:25 | hashcode:31 | unused:1 | age:4 | biased_lock:0 | 01 | Normal |
|--------------------------------------------------------------------|--------------------|
| thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | 01 | Biased |
|--------------------------------------------------------------------|--------------------|
| ptr_to_lock_record:62 | 00 | Lightweight Locked (轻量级锁🔰) |
|--------------------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:62 | 10 | Heavyweight Locked |
|--------------------------------------------------------------------|--------------------|
| | 11 | Marked for GC |
|--------------------------------------------------------------------|--------------------|
(2).Monitor_synorized原理
Monitor 被翻译为监视器或管程。
每个 Java 对象都可以关联一个 Monitor 对象,如果使用synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针。
Monitor 结构如下
- 刚开始 Monitor 中
Owner 为 null
。 - 当
Thread-2
执行 synchronized(obj) 就会将Monitor
的所有者 Owner 置为 Thread-2,Monitor中只能有一个 Owner
。 - 在 Thread-2 上锁的过程中,如果
Thread-3,Thread-4,Thread-5
也来执行 synchronized(obj),就会进入EntryList BLOCKED
(阻塞队列) Thread-2
执行完同步代码块的内容,然后唤醒EntryList
中等待的线程来竞争锁,竞争时是非公平的。- 图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程,后面讲wait-notify 时会分析。
注意:
- synchronized 必须是进入同一个对象的 monitor 才有上述的效果
- 不加 synchronized 的对象不会关联监视器,不遵从以上规则
(3).Monitor_字节码角度
static final Object lock = new Object();
static int counter = 0;public static void main(String[] args) {synchronized (lock) {counter++;}
}
对应的字节码为
public static void main(java.lang.String[]);descriptor:([Ljava/lang/String;)Vflags:ACC_PUBLIC,ACC_STATICCode:stack=2,locals=3,args_size=10:getstatic #2 // <- lock引用 (synchronized开始)3:dup4:astore_1 // lock引用 -> slot 15:monitorenter // 将 lock对象 MarkWord 置为 Monitor 指针6:getstatic #3 // <- i9:iconst_1 // 准备常数 110:iadd // +111:putstatic #3 // -> i14:aload_1 // <- lock引用15:monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList16:goto 2419:astore_2 // e -> slot 2 20:aload_1 // <- lock引用21:monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList22:aload_2 // <- slot 2 (e)23:athrow // throw e24:returnException table:from to target type6 16 19any19 22 19anyLineNumberTable:line 8:0line 9:6line 10:14line 11:24LocalVariableTable:Start Length Slot Name Signature0 25 0args[Ljava/lang/String;StackMapTable:number_of_entries=2frame_type=255 /* full_frame */offset_delta=19locals=[class "[Ljava/lang/String;",class java/lang/Object]stack=[class java/lang/Throwable]frame_type=250 /* chop */offset_delta=4
(4).小故事
故事角色
- 老王 - JVM
- 小南 - 线程
- 小女 - 线程
- 房间 - 对象
- 房间门上 - 防盗锁 - Monitor
- 房间门上 - 小南书包 - 轻量级锁
- 房间门上 - 刻上小南大名 - 偏向锁
- 批量重刻名 - 一个类的偏向锁撤销到达 20 阈值
- 不能刻名字 - 批量撤销该类对象的偏向锁,设置该类不可偏向
小南要使用房间保证计算不被其它人干扰(原子性),最初,他用的是防盗锁,当上下文切换时,锁住门。这样,即使他离开了,别人也进不了门,他的工作就是安全的。
但是,很多情况下没人跟他来竞争房间的使用权。小女是要用房间,但使用的时间上是错开的,小南白天用,小女晚上用。每次上锁太麻烦了,有没有更简单的办法呢?
小南和小女商量了一下,约定不锁门了,而是谁用房间,谁把自己的书包挂在门口,但他们的书包样式都一样,因此每次进门前得翻翻书包,看课本是谁的,如果是自己的,那么就可以进门,这样省的上锁解锁了。万一书包不是自己的,那么就在门外等,并通知对方下次用锁门的方式。
后来,小女回老家了,很长一段时间都不会用这个房间。小南每次还是挂书包,翻书包,虽然比锁门省事了,但仍然觉得麻烦。
于是,小南干脆在门上刻上了自己的名字:【小南专属房间,其它人勿用】,下次来用房间时,只要名字还在,那么说明没人打扰,还是可以安全地使用房间。如果这期间有其它人要用这个房间,那么由使用者将小南刻的名字擦掉,升级为挂书包的方式。
同学们都放假回老家了,小南就膨胀了,在 20 个房间刻上了自己的名字,想进哪个进哪个。后来他自己放假回老家了,这时小女回来了(她也要用这些房间),结果就是得一个个地擦掉小南刻的名字,升级为挂书包的方式。老王觉得这成本有点高,提出了一种批量重刻名的方法,他让小女不用挂书包了,可以直接在门上刻上自己的名字.
后来,刻名的现象越来越频繁,老王受不了了:算了,这些房间都不能刻名了,只能挂书包
(5).synchorized优化原理_轻量级锁 (解决防盗锁)
轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
轻量级锁对使用者是透明的,即语法仍然是 synchronized
假设有两个方法同步块,利用同一个对象加锁
package com.jsxs.utils;import lombok.extern.slf4j.Slf4j;/*** @Author Jsxs* @Date 2023/10/2 16:17* @PackageName:com.jsxs.utils* @ClassName: ExerciseSell* @Description: TODO* @Version 1.0*/
@Slf4j(topic = "c.sell")
public class ExerciseTransfer {static final Object obj = new Object();public static void method1() {synchronized (obj) {// 同步块 Amethod2();}}public static void method2() {synchronized (obj) {// 同步块 B}}
}
- 创建锁记录( Lock Record )对象 JVM层面的,每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的
Mark Word
。
- 让锁记录中
Object reference
指向锁对象,并尝试用cas
替换Object 的 Mark Word
,将Mark Word
的值存入锁记录。
- 如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁,这时图示如下
-
如果 cas 失败,有两种失败情况
- 如果是
其它线程已经持有了该 Object 的轻量级锁 也就是(00)
,这时表明有竞争,进入锁膨胀过程。 - 如果是
自己执行了 synchronized 锁重入(锁中套锁)
,那么再添加一条 Lock Record 作为重入的计数。
- 如果是
- 当退出
synchronized
代码块(解锁时)如果有取值为null
的锁记录,表示有重入。这时重置锁记录,表示重入计数减一;也就是恢复。
- 当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用
cas 将 Mark Word 的值恢复给对象头
原本是01,就恢复成01。- 成功,则解锁成功。
- 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程。
总结: 轻量级锁实质上就是交换对象头中的 Mark Word 的操作。
(6).synchorized优化原理_锁膨胀
如果在尝试加轻量级锁的过程中,CAS 操作无法成功
,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
static Object obj = new Object();public static void method1() {synchronized( obj ) {// 同步块}
}
- 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁。
- 这时 Thread-1 加轻量级锁失败,进入锁膨胀流程
- 即为
Object
对象申请Monitor
锁,让Object 指向重量级锁地址 (10)
。 - 然后自己进入
Monitor
的EntryList BLOCKED
(阻塞队列)
- 即为
- 当
Thread-0
退出同步块解锁时,使用cas
将Mark Word
的值恢复给对象头,会出现失败(因为在恢复前Thread -1就让object成为01了
)。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程。
(7).synchorized优化原理_自旋锁
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。
- 自旋重试成功的情况
避免了进入阻塞队列的操作,一直在自旋。
- 自旋重试失败的情况
也就是说线程2一直在自旋但是线程一一直不释放锁,最终自旋失败只能进入阻塞队列中去。
- 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
- 在
Java 6
之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。 Java 7
之后不能控制是否开启自旋功能
(8).synchorized优化原理_偏向锁 (解决书包翻书)
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。
Java 6
中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有。
列如:
重复加锁,每次锁重入的时候都要进行锁重入CAS的操作
static final Object obj = new Object();public static void m1() {synchronized (obj) {// 同步块 Am2();}}public static void m2() {synchronized (obj) {// 同步块 Bm3();}}public static void m3() {synchronized (obj) { // 同步块 C}}
(9).偏向锁细讲_状态
回忆一下对象头格式
|-------------------------------------------------------|--------------------|
| Mark Word (32 bits) | State |
|-------------------------------------------------------|--------------------|
| hashcode:25 | age:4 | biased_lock:0 (没有启用偏向锁 🔰) | 01 | Normal (正常状态🔰) |
|-------------------------------------------------------|--------------------|
| thread:23 (线程id) | epoch:2 | age:4 | biased_lock:1 (启用偏向锁 🔰) | 01 | Biased (偏向锁状态) |
|-------------------------------------------------------|--------------------|
| ptr_to_lock_record:30 | 00 | Lightweight Locked (轻量级锁🔰)|
|-------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:30 | 10 | Heavyweight Locked (重量级锁🔰) |
|-------------------------------------------------------|--------------------|
| | 11 | Marked for GC |
|-------------------------------------------------------|--------------------|
一个对象创建时:
-
如果开启了 偏向锁(默认开启101),那么对象创建后,
markword 值为 0x05 即最后 3 位为 101
,这时它的thread、epoch、age 都为 0
。 -
偏向锁是默认是
延迟(默认两秒)
的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数- XX:BiasedLockingStartupDelay=0
来禁用延迟。
-
如果没有开启偏向锁,那么对象创建后,
markword 值为 0x01 即最后 3 位为 001
,这时它的 hashcode、age 都为 0,第一次用到 hashcode 时才会赋值。
- 正常情况下: 我们不能查看消息头
1.我们需要先导入依赖
<dependency><groupId>org.openjdk.jol</groupId><artifactId>jol-core</artifactId><version>0.10</version></dependency>
2.测试如下
package com.jsxs.utils;import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;/*** @Author Jsxs* @Date 2023/10/3 11:41* @PackageName:com.jsxs.utils* @ClassName: TestBiased* @Description: TODO* @Version 1.0*/@Slf4j(topic = "c.test")
public class TestBiased {public static void main(String[] args) {// 使用jol-core的包获取整个消息头的信息System.out.println(ClassLayout.parseInstance(new Dog()).toPrintable());try {Thread.sleep(4000); // 因为偏向锁具有延迟性,所以我们先延迟四秒再看} catch (InterruptedException e) {e.printStackTrace();}// 使用jol-core的包获取整个消息头的信息System.out.println(ClassLayout.parseInstance(new Dog()).toPrintable());}
}class Dog {
}
- 测试加锁前后: 我们对虚拟机参数做了修改
package com.jsxs.utils;import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;/*** @Author Jsxs* @Date 2023/10/3 11:41* @PackageName:com.jsxs.utils* @ClassName: TestBiased* @Description: TODO* @Version 1.0*/@Slf4j(topic = "c.test")
public class TestBiased {public static void main(String[] args) {// 使用jol-core的包获取整个消息头的信息Dog d = new Dog();System.out.println("加锁前: ");System.out.println(ClassLayout.parseInstance(d).toPrintable());synchronized (d){System.out.println("加锁时: ");System.out.println(ClassLayout.parseInstance(d).toPrintable());}System.out.println("解锁后: ");System.out.println(ClassLayout.parseInstance(d).toPrintable());}
}class Dog {
}
注意: 处于偏向锁的对象解锁后,线程 id 仍存储于对象头中
- 测试禁用
在上面测试代码运行时在添加 VM 参数 -XX:-UseBiasedLocking
禁用偏向锁。
优先级: 有偏向锁优先偏向锁,否则第二就是轻量级锁
输出
11:13:10.018 c.TestBiased [t1] - synchronized 前 (正常状态)
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
11:13:10.021 c.TestBiased [t1] - synchronized 中 (轻量级锁)
00000000 00000000 00000000 00000000 00100000 00010100 11110011 10001000
11:13:10.021 c.TestBiased [t1] - synchronized 后
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
- 测试 hashCode
- 正常状态对象一开始是没有 hashCode 的,第一次调用才生成。
(10).撤销偏向锁 - 调用对象 hashCode
调用了对象的 hashCode
,但偏向锁的对象 MarkWord 中存储的是线程 id,如果调用 hashCode 会导致偏向锁被撤销
.
- 轻量级锁会在锁记录中记录
hashCode
- 重量级锁会在
Monitor
中记录hashCode
在调用 hashCode 后使用偏向锁,记得去掉 -XX:-UseBiasedLocking
11:22:10.386 c.TestBiased [main] - 调用 hashCode:1778535015
11:22:10.391 c.TestBiased [t1] - synchronized 前 (正常锁)
00000000 00000000 00000000 01101010 00000010 01001010 01100111 00000001
11:22:10.393 c.TestBiased [t1] - synchronized 中 (轻量级锁)
00000000 00000000 00000000 00000000 00100000 11000011 11110011 01101000
11:22:10.393 c.TestBiased [t1] - synchronized 后 (正常锁)
00000000 00000000 00000000 01101010 00000010 01001010 01100111 00000001
使用hashCode之后,偏向锁会失效。
(11).撤销偏向锁 - 其它线程使用对象
当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁。
加等待的主要原因是先不存在竞争让线程1解锁之后,再进行唤醒线程2.
package com.jsxs.utils;import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;/*** @Author Jsxs* @Date 2023/10/3 11:41* @PackageName:com.jsxs.utils* @ClassName: TestBiased* @Description: TODO* @Version 1.0*/@Slf4j(topic = "c.test")
public class TestBiased {public static void main(String[] args) throws InterruptedException {test2();}private static void test2() throws InterruptedException {Dog d = new Dog();Thread t1 = new Thread(() -> {synchronized (d) { //这里加的时偏向锁log.debug(ClassLayout.parseInstance(d).toPrintable());}synchronized (TestBiased.class) {TestBiased.class.notify(); // 在这里等待唤醒}}, "t1");t1.start();Thread t2 = new Thread(() -> {synchronized (TestBiased.class) {try {TestBiased.class.wait(); // 在这里进行等待} catch (InterruptedException e) {e.printStackTrace();}}log.debug(ClassLayout.parseInstance(d).toPrintable());synchronized (d) {log.debug(ClassLayout.parseInstance(d).toPrintable());}log.debug(ClassLayout.parseInstance(d).toPrintable());}, "t2");t2.start();}
}class Dog {
}
[t1] - 00000000 00000000 00000000 00000000 00011111 01000001 00010000 00000101 (偏向锁)
[t2] - 00000000 00000000 00000000 00000000 00011111 01000001 00010000 00000101 (偏向锁)
[t2] - 00000000 00000000 00000000 00000000 00011111 10110101 11110000 01000000 (轻量级锁)
[t2] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 (正常锁)
(12).撤销偏向锁 - 调用 wait/notify
会造成 偏向级锁转换为重量级锁
public static void main(String[] args) throws InterruptedException {Dog d = new Dog();Thread t1 = new Thread(() -> {log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));synchronized (d) {log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));try {d.wait(); //调用睡眠} catch (InterruptedException e) {e.printStackTrace();}log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));}}, "t1");t1.start();new Thread(() -> {try {Thread.sleep(6000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (d) {log.debug("notify");d.notify(); // 调用唤醒}}, "t2").start();}
[t1] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101 偏向锁
[t1] - 00000000 00000000 00000000 00000000 00011111 10110011 11111000 00000101 偏向锁
[t2] - notify 重量级锁
[t1] - 00000000 00000000 00000000 00000000 00011100 11010100 00001101 11001010
(13).批量偏向锁 - 批量对象
对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2
,重偏向会重置对象的 Thread ID。
当撤销偏向锁阈值超过 20 次
后,jvm 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至加锁线程。
package com.jsxs.utils;import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;import java.util.Vector;/*** @Author Jsxs* @Date 2023/10/3 11:41* @PackageName:com.jsxs.utils* @ClassName: TestBiased* @Description: TODO* @Version 1.0*/@Slf4j(topic = "c.test")
public class TestBiased {public static void main(String[] args) throws InterruptedException {test3();}private static void test3() throws InterruptedException {Vector<Dog> list = new Vector<>();Thread t1 = new Thread(() -> {for (int i = 0; i < 30; i++) {Dog d = new Dog();list.add(d);// 将30个偏向锁偏向t1synchronized (d) {log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());}}synchronized (list) {list.notify(); // 最后唤醒 t2}}, "t1");t1.start();Thread t2 = new Thread(() -> {synchronized (list) {try {list.wait(); // 存在这个撤销锁} catch (InterruptedException e) {e.printStackTrace();}}log.debug("===============> ");for (int i = 0; i < 30; i++) {Dog d = list.get(i);log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());// 将锁偏向给t2,t1的偏向锁相当于全部撤销synchronized (d) {log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());}log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());}}, "t2");t2.start();}
}class Dog {}
[t1] - 0 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 1 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 2 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 3 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 4 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 5 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 6 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 7 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 8 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 9 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 10 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 11 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 12 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 13 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 14 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 15 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 16 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 17 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 18 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 20 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 21 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 22 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 23 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 24 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 25 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 26 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 27 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 28 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - ===============> 以上偏向于t1 ⭐
[t2] - 0 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 0 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 0 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 1 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 1 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 1 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 2 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 2 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 2 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 3 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 3 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 3 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 4 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 4 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 4 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 5 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 5 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 5 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 6 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 6 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 6 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
======>以上经过20论反复反复撤销,达到20的阈值就会出现重现偏移锁 ⭐
[t2] - 7 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 25 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 25 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 25 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 26 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 26 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 26 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 27 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 27 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 27 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 28 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 28 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 28 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
(14).批量撤销锁 - 批量对象
当撤销偏向锁阈值超过 40次后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的。
package com.jsxs.utils;import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;import java.util.Vector;
import java.util.concurrent.locks.LockSupport;/*** @Author Jsxs* @Date 2023/10/3 11:41* @PackageName:com.jsxs.utils* @ClassName: TestBiased* @Description: TODO* @Version 1.0*/@Slf4j(topic = "c.test")
public class TestBiased {public static void main(String[] args) throws InterruptedException {test4();}static Thread t1, t2, t3;private static void test4() throws InterruptedException {Vector<Dog> list = new Vector<>();int loopNumber = 39;// 线程1循环39次,加 39个线程偏向锁t1 = new Thread(() -> {for (int i = 0; i < loopNumber; i++) {Dog d = new Dog();list.add(d);synchronized (d) {log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());}}LockSupport.unpark(t2);}, "t1");t1.start();// 线程2会发生批量重偏向(前20个撤销) 20~39进行批量重偏向 ⭐t2 = new Thread(() -> {LockSupport.park(); // 等待log.debug("===============> ");for (int i = 0; i < loopNumber; i++) {Dog d = list.get(i);log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());synchronized (d) {log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());}log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());}LockSupport.unpark(t3);}, "t2");t2.start();// 这里会继续撤销20个重偏向锁。⭐t3 = new Thread(() -> {LockSupport.park(); // 等待log.debug("===============>3 ");for (int i = 0; i < loopNumber; i++) {Dog d = list.get(i);log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());synchronized (d) {log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());}log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());}}, "t3");t3.start();t3.join();// 第41个偏向锁.log.debug(ClassLayout.parseInstance(new Dog()).toPrintable()); // 我们再打印不应该是偏向锁了。应该是01重量级}
}class Dog {}
(15).锁消除
锁消除:
package com.jsxs.sample;/*** @Author Jsxs* @Date 2023/9/29 13:45* @PackageName:com.jsxs.sample* @ClassName: MyBenchmark* @Description: TODO* @Version 1.0*/import java.util.Arrays;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.Warmup;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;@Fork(1)
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
public class MyBenchmark {static int x = 0;@Benchmarkpublic void a() throws Exception {x++; // 不加锁进行 ++}@Benchmarkpublic void b() throws Exception {Object object= new Object(); // 局部变量objectsynchronized (o) { // 加锁后进行 ++x++;}}public static void main(String[] args) throws RunnerException {Options opt = new OptionsBuilder().include(MyBenchmark.class.getSimpleName()).forks(1).build();new Runner(opt).run();}}
我们发现加锁和不加锁相差不大! 主要原因是因为JVM进行了优化加锁,
JIT: 会对我们的字节码进行即时编译进行优化,因为分析道局部变量object逃离不掉作用于的范围,也就是说是私有的,所以加锁会没有任何意义,所以JIT会给我们优化掉。
-XX:-EliminateLocks 关掉JIT即时编译功能
(15).锁粗化
对相同对象多次加锁,导致线程发生多次重入,可以使用锁粗化方式来优化,这不同于之前讲的细分锁的粒度。
相关文章:
135.【JUC并发编程_01】
JUC 并发编程 (一)、基本概述1.概述 (二)、进程与线程1.进程与线程(1).进程_介绍(2).线程_介绍(3).进程与线程的区别 2.并行和并发(1).并发_介绍(2).并行_介绍(3).并行和并发的区别 3.应用(1).异步调用_较少等待时间(2).多线程_提高效率 (三)、Java 线程1.创建线程和运行线程(1…...
VC++创建windows服务程序
目录 1.关于windows标准可执行程序和服务程序 2.服务相关整理 2.1 VC编写服务 2.2 服务注册 2.3 服务卸载 2.4 启动服务 2.5 关闭服务 2.6 sc命令 2.7 查看服务 3.标准程序 3.1 后台方式运行标准程序 3.2 查找进程 3.3 终止进程 以前经常在Linux下编写服务器程序…...
连续爆轰发动机
0.什么是爆轰 其反应区前沿为一激波。反应区连同前驱激波称为爆轰波。爆轰波扫过后,反应区介质成为高温高压的爆轰产物。能够发生爆轰的系统可以是气相、液相、固相或气-液、气-固和液-固等混合相组成的系统。通常把液、固相的爆轰系统称为炸药。 19世纪80年代初&a…...
交通物流模型 | 基于时空注意力融合网络的城市轨道交通假期短时客流预测
短时轨道交通客流预测对于交通运营管理非常重要。新兴的深度学习模型有效提高了预测精度。然而,大部分现有模型主要针对常规工作日或周末客流进行预测。由于假期客流的突发性和无规律性,仅有一小部分研究专注于假期客流预测。为此,本文提出一个全新的时空注意力融合网络(ST…...
2.2.1 嵌入式工程师必备软件
1 文件比较工具 在开发过程中,不论是对代码的对比,还是对log的对比,都是必不可不少的,通过对比,我们可以迅速找到差异,定位问题。当前常用的对比工具有:WinMerge,Diffuse,Beyond Compare,Altova DiffDog,AptDiff,Code Compare等。这里推荐使用Beyond Compare,它不…...
深入了解 RabbitMQ:高性能消息中间件
目录 引言:一、RabbitMQ 介绍二、核心概念三、工作原理四、应用场景五、案例实战 引言: 在现代分布式系统中,消息队列成为了实现系统间异步通信、削峰填谷以及解耦组件的重要工具。而RabbitMQ作为一个高效可靠的消息队列解决方案,…...
【数据库——MySQL】(14)过程式对象程序设计——游标、触发器
目录 1. 游标1.1 声明游标1.2 打开游标1.3 读取游标1.4 关闭游标1.5 游标示例 2. 触发器2.1 创建触发器2.2 修改触发器2.3 删除触发器2.4 触发器类型2.5 触发器示例 参考书籍 1. 游标 游标一般和存储过程一起配合使用。 1.1 声明游标 要使用游标,需要用到 DECLAR…...
位移贴图和法线贴图的区别
位移贴图和法线贴图都是用于增强模型表面细节和真实感的纹理贴图技术,但是它们之间也存在着差异。 1、什么是位移贴图 位移贴图:位移贴图通过在模型顶点上定义位移值来改变模型表面的形状。该贴图包含了每个像素的高度值信息,使得模型的细节…...
【typescript】面向对象(下篇),包含接口,属性的封装,泛型
假期第八篇,对于基础的知识点,我感觉自己还是很薄弱的。 趁着假期,再去复习一遍 面向对象:程序中所有的操作都需要通过对象来完成 计算机程序的本质就是对现实事物的抽象,抽象的反义词是具体。比如照片是对一个具体的…...
基于SpringBoot的视频网站系统
目录 前言 一、技术栈 二、系统功能介绍 用户信息管理 视频分享管理 视频排名管理 交流论坛管理 留言板管理 三、核心代码 1、登录模块 2、文件上传模块 3、代码封装 前言 使用旧方法对视频信息进行系统化管理已经不再让人们信赖了,把现在的网络信息技术运…...
23.3 Bootstrap 框架4
1. 轮播 1.1 轮播样式 在Bootstrap 5中, 创建轮播(Carousel)的相关类名及其介绍: * 1. carousel: 轮播容器的类名, 用于标识一个轮播组件. * 2. slide: 切换图片的过渡和动画效果. * 3. carousel-inner: 轮播项容器的类名, 用于包含轮播项(轮播图底下椭圆点, 轮播的过程可以显…...
ESP32设备驱动-I2C-LCD1602显示屏驱动
I2C-LCD1602显示屏驱动 1、LCD1602介绍 LCD1602液晶显示器是广泛使用的一种字符型液晶显示模块。它是由字符型液晶显示屏(LCD)、控制驱动主电路HD44780及其扩展驱动电路HD44100,以及少量电阻、电容元件和结构件等装配在PCB板上而组成。 通过前面的实例我们知道,并口方式…...
vs工具箱在哪里找
VS工具箱在标题栏 视图->工具箱...
uniapp 事件委托失败 获取不到dataset
问题: v-for 多个span ,绑定点击事件 代码:view里包着一个span, <view class"status-list" tap"search"><span class"status-item" v-for"(key,index) in statusList" :key"index" :data-key"k…...
windows系统下pycharm配置anaconda
参考:超详细的PycharmAnconda安装配置教程_pycharm conda_罅隙的博客-CSDN博客 下载好anaconda安装后,比如我们安装在D盘anaconda文件夹下,在pycharm配置好环境激活时出现问题,可能是电脑没有配置环境变量 需要将一下4行添加到电…...
2023年CSP-J真题详解+分析数据
目录 亲身体验 江苏卷 选择题 阅读程序题 阅读程序(1) 判断题 单选题 阅读程序(2) 判断题 单选题 阅读程序(3) 判断题 单选题 完善程序题 完善程序(1) 完善程序(2) 2023CSP-J江苏卷详解 小结 亲身体验 2023年的CSP-J是在9月16日9:30--11:30进行…...
10.3 调试事件转存进程内存
我们继续延申调试事件的话题,实现进程转存功能,进程转储功能是指通过调试API使获得了目标进程控制权的进程,将目标进程的内存中的数据完整地转存到本地磁盘上,对于加壳软件,通常会通过加密、压缩等手段来保护其代码和数…...
深度学习实战基础案例——卷积神经网络(CNN)基于MobileNetV3的肺炎识别|第3例
文章目录 前言一、数据集介绍二、前期工作三、数据集读取四、构建CA注意力模块五、构建模型六、开始训练 前言 Google公司继MobileNetV2之后,在2019年发表了它的改进版本MobileNetV3。而MobileNetV3共有两个版本,分别是MobileNetV3-Large和MobileNetV2-…...
机器学习 面试/笔试题(更新中)
1. 生成模型 VS 判别模型 生成模型: 由数据学得联合概率分布函数 P ( X , Y ) P(X,Y) P(X,Y),求出条件概率分布 P ( Y ∣ X ) P(Y|X) P(Y∣X)的预测模型。 朴素贝叶斯、隐马尔可夫模型、高斯混合模型、文档主题生成模型(LDA)、限制玻尔兹曼机…...
【算法题】100019. 将数组分割成最多数目的子数组
插: 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。 坚持不懈,越努力越幸运,大家一起学习鸭~~~ 题目: 给你一个只包含 非负 整数的数组 n…...
commons-io工具类常用方法
commons-io是Apache Commons项目的一个模块,提供了一系列处理I/O(输入/输出)操作的工具类和方法。它旨在简化Java I/O编程,并提供更多的功能和便利性。 读取文件内容为字符串 String path"C:\\Users\\zhang\\Desktop\\myyii\…...
【Typescript】面向对象(上篇),包含类,构造函数,继承,super,抽象类
假期第七篇,对于基础的知识点,我感觉自己还是很薄弱的。 趁着假期,再去复习一遍 面向对象:程序中所有的操作都需要通过对象来完成 计算机程序的本质就是对现实事物的抽象,抽象的反义词是具体。比如照片是对一个具体的…...
【python】python中字典的用法记录
文章目录 序言1. 字典的创建和访问2. 字典如何添加元素3. 字典作为函数参数4. 字典排序 序言 总结字典的一些常见用法 1. 字典的创建和访问 字典是一种可变容器类型,可以存储任意类型对象 key : value,其中value可以是任何数据类型,key必须…...
基于Java的大学生心理咨询系统设计与实现(源码+lw+部署文档+讲解等)
文章目录 前言具体实现截图论文参考详细视频演示为什么选择我自己的网站自己的小程序(小蔡coding)有保障的售后福利 代码参考源码获取 前言 💗博主介绍:✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域优质创作…...
Redis-双写一致性
双写一致性 双写一致性解决方案延迟双删(有脏数据的风险)分布式锁(强一致性,性能比较低)异步通知(保证数据的最终一致性,高并发情况下会出现短暂的不一致情况) 双写一致性 当修改了数…...
CustomTkinter:创建现代、可定制的Python UI
文章目录 介绍安装设置外观与主题外观模式主题设置自定义主题颜色窗口缩放CTkFont字体设置CTkImage图片Widgets窗口部件CTk Windows窗口CTk窗口CTkInputDialog对话框CTkToplevel顶级窗口布局pack布局palce布局Grid 网格布局Frames 框架Frames滚动框架...
华为OD机试真题【不含 101 的数】
1、题目描述 【不含 101 的数】 【题目描述】 小明在学习二进制时,发现了一类不含 101的数,也就是: 将数字用二进制表示,不能出现 101 。 现在给定一个整数区间 [l,r] ,请问这个区间包含了多少个不含 101 的数&#…...
Spring IoC和DI详解
IOC思想 IoC( Inversion of Control,控制反转) 不是一门具体技术,而是一种设计思想, 是一种软件设计原则,它将应用程序的控制权(Bean的创建和依赖关系)从应用程序代码中解耦出来&am…...
mysql-binlog
1. 常用的binlog日志操作命令 1. 查看bin-log是否开启 show variables like log_%;2. 查看所有binlog日志列表 show master logs;3.查看master状态 show master status;4. 重置(清空)所有binlog日志 reset master;2. 查看binlog日志内容 1、使用mysqlb…...
通过BeanFactotyPostProcessor动态修改@FeignClient的path
最近项目有个需求,要在启动后,动态修改FeignClient的请求路径,网上找到的基本都是在FeignClient里使用${…},通过配置文件来定义Feign的接口路径,这并不能满足我们的需求 由于某些特殊原因,我们的每个接口…...
网站服务器提供商/百度官方平台
■■■■■■■■■■■■■■■■■■■↓↓↓↓↓↓↓↓↓ Hibernate框架 —— 映射配置文件基本详细配置↓↓↓↓↓↓↓↓↓↓↓■■■■■■■■■■■■■■■■■■…...
淘宝客的网站怎么做/大专网络营销专业好不好
1.主键是能确定一条记录的唯一标识,比如,一条记录包括身份正号,姓名,年龄。 身份证号是唯一能确定你这个人的,其他都可能有重复,所以,身份证号是主键。 2.外键用于与另一张表的关联。是能确定另…...
自助建立网站/快速提升网站关键词排名
AC自动机是KMP的多串形式,当文本串失配时,AC自动机的fail指针告诉我们应该跳到哪里去继续匹配(跳到当前匹配串的最长后缀去),所以AC自动机的状态是有限的 但是AC自动机具有不确定性, 比如要求x结点的孩子c的…...
网站开发项目外包/百度知道网页版进入
说明: 以下操作是管理员权限,系统是Ubuntu16.04 安装npm工具,指令如下 apt install npm 指定apidoc版本安装,指令如下 npm install apidoc0.17.6 -g 安装之后,使用如下指令出现警告信息 rootwgl-virtual-machine…...
web前端开发技术有哪些/seo三人行论坛
题目链接:hdu 4770 Lights Against Dudely 题目大意:在一个N*M的银行里。有N*M个房间,‘#’代表牢固的房间,‘.‘代表的是脆弱的房间。脆弱的房间个数不会超过15个,如今为了确保安全,要在若干个脆弱的房间上…...
广西网站建设产品优化/网店运营公司
1.wxml 1.时间戳转换成时间格式倒计时: <view stylemargin:10px 20px>倒计时:<text>{{hhh1}}:{{mmm1}}:{{sss1}}</text> </view> 2.倒计时60秒 <view stylemargin:10px 20px>倒计时:<text>{{time}}s<…...