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"?>
<configuration
xmlns: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);
}
@Benchmark
public 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();
}
@Benchmark
public 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") {
@Override
public 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() {
@Override
public 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>() {
@Override
public 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"){
@Override
public 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
@Override
public 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
@Override
public 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
@Override
public void run() {
log.debug("进入睡眠 .... ");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
// 当线程睡眠被打断之后,会报异常....
log.debug("wake up ....");
e.printStackTrace();
}
}
};
thread.start();
Thread.sleep(1000); //主线程睡眠 1s
log.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); // 因为当阻塞释放后才会运行,所以结果为10
log.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(){
@Override
public 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); // 主线程睡眠1s
log.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状态
@Override
public void run() {
log.debug("running..");
}
};
// 2.阻塞状态
Thread t2 = new Thread("t2") { //2. Runnable (可运行、运行、阻塞)
@Override
public void run() {
while (true) {
}
}
};
t2.start();
Thread t3 = new Thread("t3") { //3. Runnable (运行状态)
@Override
public void run() {
log.debug("running..");
}
};
t3.start();
Thread t4 = new Thread("t4") {
@Override
public void run() {
synchronized (Sync.class){
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t4.start();
Thread t5 = new Thread("t5") {
@Override
public void run() {
try {
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t5.start();
Thread t6 = new Thread("t6") {
@Override
public 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") {
@Override
public 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"){
@Override
public 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: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=0
0: bipush 10 // 赋值为10
2: istore_0
3: iinc 0, 1 // 做自增1
6: return // 返回结果
LineNumberTable:
line 10: 0
line 11: 3
line 12: 6
LocalVariableTable:
Start Length Slot Name Signature
3 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 {
@Override
public 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() {
// 是否安全 : 不能确定是否安全,看foo
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 的行为是不确定的,可能导致不安全的发生,被称之为外星方法
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 + "个顾客") {
@Override
public void run() {
// 顾客买票 : 买票的数目为 0~5
int 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 + "个顾客") {
@Override
public void run() {
// 顾客买票 : 买票的数目为 0~5
int 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~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) {
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;)V
flags:ACC_PUBLIC,ACC_STATIC
Code:
stack=2,locals=3,args_size=1
0:getstatic #2 // <- lock引用 (synchronized开始)
3:dup
4:astore_1 // lock引用 -> slot 1
5:monitorenter // 将 lock对象 MarkWord 置为 Monitor 指针
6:getstatic #3 // <- i
9:iconst_1 // 准备常数 1
10:iadd // +1
11:putstatic #3 // -> i
14:aload_1 // <- lock引用
15:monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList
16:goto 24
19:astore_2 // e -> slot 2
20:aload_1 // <- lock引用
21:monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList
22:aload_2 // <- slot 2 (e)
23:athrow // throw e
24:return
Exception table:
from to target type
6 16 19any
19 22 19any
LineNumberTable:
line 8:0
line 9:6
line 10:14
line 11:24
LocalVariableTable:
Start Length Slot Name Signature
0 25 0args[Ljava/lang/String;
StackMapTable:number_of_entries=2
frame_type=255 /* full_frame */
offset_delta=19
locals=[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) {
// 同步块 A
method2();
}
}
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) {
// 同步块 A
m2();
}
}
public static void m2() {
synchronized (obj) {
// 同步块 B
m3();
}
}
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个偏向锁偏向t1
synchronized (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;
@Benchmark
public void a() throws Exception {
x++; // 不加锁进行 ++
}
@Benchmark
public void b() throws Exception {
Object object= new Object(); // 局部变量object
synchronized (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).锁粗化
对相同对象多次加锁,导致线程发生多次重入,可以使用锁粗化方式来优化,这不同于之前讲的细分锁的粒度。