剑指JUC原理-8.Java内存模型
- 👏作者简介:大家好,我是爱吃芝士的土豆倪,24届校招生Java选手,很高兴认识大家
- 📕系列专栏:Spring源码、JUC源码
- 🔥如果感觉博主的文章还不错的话,请👍三连支持👍一下博主哦
- 🍂博主正在努力完成2023计划中:源码溯源,一探究竟
- 📝联系方式:nhs19990716,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬👀
文章目录
- Java 内存模型
- 可见性
- 退不出的循环
- 解决方法
- 可见性 vs 原子性
- 模式之两阶段终止
- 同步模式之 Balking
- 定义
- 实现
- 有序性
- 指令级并行原理
- 名词
- Clock Cycle Time
- CPI
- IPC
- CPU 执行时间
- 鱼罐头的故事
- 指令重排序优化
- 支持流水线的处理器
- SuperScalar 处理器
- 诡异的结果
- 解决方法
- volatile 原理
- 如何保证可见性
- 如何保证有序性
- double-checked locking 问题
- double-checked locking 解决
- 可见性
- 有序性
- happens-before
- 线程解锁 m 之前对变量的写,对于接下来对 m 加锁的其它线程对该变量的读可见
- 线程对 volatile 变量的写,对接下来其它线程对该变量的读可见
- 线程 start 前对变量的写,对该线程开始后对该变量的读可见
- 线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用 t1.isAlive() 或 t1.join()等待它结束)
- 线程 t1 打断 t2(interrupt)前对变量的写,对于其他线程得知 t2 被打断后对变量的读可见(通过t2.interrupted 或 t2.isInterrupted)
- 具有传递性,如果 x hb-> y 并且 y hb-> z 那么有 x hb-> z ,配合 volatile 的防指令重排,有下面的例子
- balking 模式习题
- 线程安全单例习题
- 实现1
- 实现2(※※※重点难点※※※)
- 实现3:
- 实现4:DCL
- 实现5
Java 内存模型
JMM 即 Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、
CPU 指令优化等。
JMM 体现在以下几个方面
- 原子性 - 保证指令不会受到线程上下文切换的影响
- 可见性 - 保证指令不会受 cpu 缓存的影响
- 有序性 - 保证指令不会受 cpu 指令并行优化的影响
可见性
退不出的循环
先来看一个现象,main 线程对 run 变量的修改对于 t 线程不可见,导致了 t 线程无法停止:
static boolean run = true;public static void main(String[] args) throws InterruptedException {Thread t = new Thread(()->{while(run){// ....}});t.start();sleep(1);run = false; // 线程t不会如预想的停下来}
为什么呢?分析一下:
初始状态, t 线程刚开始从主内存读取了 run 的值到工作内存。
因为 t 线程要频繁从主内存中读取 run 的值,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中,
减少对主存中 run 的访问,提高效率
1 秒之后,main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量
的值,结果永远是旧值
解决方法
volatile(易变关键字)
它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取
它的值,线程操作 volatile 变量都是直接操作主存
可见性 vs 原子性
前面例子体现的实际就是可见性,它保证的是在多个线程之间,一个线程对 volatile 变量的修改对另一个线程可
见, 不能保证原子性,仅用在一个写线程,多个读线程的情况: 上例从字节码理解是这样的:
getstatic run // 线程 t 获取 run true
getstatic run // 线程 t 获取 run true
getstatic run // 线程 t 获取 run true
getstatic run // 线程 t 获取 run true
putstatic run // 线程 main 修改 run 为 false, 仅此一次
getstatic run // 线程 t 获取 run false
比较一下之前我们将线程安全时举的例子:两个线程一个 i++ 一个 i-- ,只能保证看到最新值,不能解决指令交错
// 假设i的初始值为0
getstatic i // 线程2-获取静态变量i的值 线程内i=0 getstatic i // 线程1-获取静态变量i的值 线程内i=0
iconst_1 // 线程1-准备常量1
iadd // 线程1-自增 线程内i=1
putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i=1 iconst_1 // 线程2-准备常量1
isub // 线程2-自减 线程内i=-1
putstatic i // 线程2-将修改后的值存入静态变量i 静态变量i=-1
注意:
- synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是synchronized 是属于重量级操作,性能相对更低。
synchronized 关键字可以确保多线程环境下的可见性,主要通过两个方面来实现:
互斥访问:当一个线程获取到某个对象的锁时,其他线程无法同时获取该对象的锁,只能等待锁释放。这样可以保证在同步块中对共享变量的修改操作是原子的,不会被其他线程中断。
内存可见性:当一个线程释放锁时,会将对共享变量的修改刷新到主内存中,而当另一个线程获取锁时,会从主内存中重新读取共享变量的值,确保看到最新的值。
以下是一个例子来说明 synchronized 关键字如何保证可见性:
public class SynchronizedExample {private static boolean flag = false;public static void main(String[] args) throws InterruptedException {Thread thread1 = new Thread(() -> {synchronized (SynchronizedExample.class) {// 修改共享变量的值flag = true;System.out.println("Thread 1: flag is set to true");}});Thread thread2 = new Thread(() -> {// 暂停一段时间,确保 thread1 先执行try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (SynchronizedExample.class) {// 在同步块中访问共享变量if (flag) {System.out.println("Thread 2: flag is true");} else {System.out.println("Thread 2: flag is false");}}});thread1.start();thread2.start();thread1.join();thread2.join();
}
}
在这个例子中,有两个线程 thread1 和 thread2,它们同时访问了共享变量 flag。首先,thread1 获取了 SynchronizedExample.class 对象的锁,并将 flag 设置为 true,然后释放锁。接着,thread2 获取了同一个锁,并在同步块中访问 flag 的值。由于 thread2 获取到了锁并读取了 flag 的最新值,因此能正确地判断出 flag 的状态。
通过 synchronized 关键字的互斥性和内存可见性的特性,确保了多线程环境下的共享变量操作的一致性和可见性。
- 如果在前面示例的死循环中加入 System.out.println() 会发现即使不加 volatile 修饰符,线程 t 也能正确看到
对 run 变量的修改了,想一想为什么?
static boolean run = true;public static void main(String[] args) throws InterruptedException {Thread t = new Thread(()->{while(run){// ....System.out.println();}});t.start();sleep(1);run = false; // 线程t不会如预想的停下来}
通过分析 System.out.println() 源码来得出结果。
synchronized 是 Java 中用于实现同步的关键字,它可以用于修饰方法和代码块。当一个线程获取了对象的锁,执行 synchronized 修饰的代码时,会将变量从主内存中拷贝到线程的本地内存中。
在 synchronized 块执行结束后,JVM 会把该线程对应的本地内存中修改过的变量刷新回主内存中,以保证不同线程间所共享的变量值的一致性。
模式之两阶段终止
// 停止标记用 volatile 是为了保证该变量在多个线程之间的可见性
// 我们的例子中,即主线程把它修改为 true 对 t1 线程可见
class TPTVolatile {private Thread thread;private volatile boolean stop = false;public void start(){thread = new Thread(() -> {while(true) {Thread current = Thread.currentThread();if(stop) {log.debug("料理后事");break;}try {Thread.sleep(1000);log.debug("将结果保存");} catch (InterruptedException e) {}// 执行监控操作}},"监控线程");thread.start();}public void stop() {stop = true;thread.interrupt(); // 如果设置为ture后,线程还在sleep状态,那么使用打断即可}
}
调用
TPTVolatile t = new TPTVolatile();
t.start();Thread.sleep(3500);
log.debug("stop");
t.stop();
结果
11:54:52.003 c.TPTVolatile [监控线程] - 将结果保存
11:54:53.006 c.TPTVolatile [监控线程] - 将结果保存
11:54:54.007 c.TPTVolatile [监控线程] - 将结果保存
11:54:54.502 c.TestTwoPhaseTermination [main] - stop
11:54:54.502 c.TPTVolatile [监控线程] - 料理后事
同步模式之 Balking
定义
Balking (犹豫)模式用在一个线程发现另一个线程或本线程已经做了某一件相同的事,那么本线程就无需再做
了,直接结束返回
实现
class TwoPhaseTermination {// 监控线程private Thread monitorThread;// 停止标记private volatile boolean stop = false;// 判断是否执行过 start 方法private boolean starting = false;// 启动监控线程public void start() {synchronized (this) {if (starting) { // falsereturn;}starting = true;monitorThread = new Thread(() -> {while (true) {Thread current = Thread.currentThread();// 是否被打断if (stop) {log.debug("料理后事");break;}try {Thread.sleep(1000);log.debug("执行监控记录");} catch (InterruptedException e) {}}}, "monitor");monitorThread.start();}}// 停止监控线程public void stop() {stop = true;monitorThread.interrupt();}
}public static void main(String[] args) throws InterruptedException {TwoPhaseTermination tpt = new TwoPhaseTermination();tpt.start();tpt.start();tpt.start();/*Thread.sleep(3500);log.debug("停止监控");tpt.stop();*/}
当前端页面多次点击按钮调用 start 时
输出
[http-nio-8080-exec-1] cn.itcast.monitor.service.MonitorService - 该监控线程已启动?(false)
[http-nio-8080-exec-1] cn.itcast.monitor.service.MonitorService - 监控线程已启动...
[http-nio-8080-exec-2] cn.itcast.monitor.service.MonitorService - 该监控线程已启动?(true)
[http-nio-8080-exec-3] cn.itcast.monitor.service.MonitorService - 该监控线程已启动?(true)
[http-nio-8080-exec-4] cn.itcast.monitor.service.MonitorService - 该监控线程已启动?(true)
一个优化就是 尽可能的让synchronized代码块中的代码较少,所以可以将不关键的因素抽取出来
public void start() {synchronized (this) {if (starting) { // falsereturn;}starting = true;}monitorThread = new Thread(() -> {while (true) {Thread current = Thread.currentThread();// 是否被打断if (stop) {log.debug("料理后事");break;}try {Thread.sleep(1000);log.debug("执行监控记录");} catch (InterruptedException e) {}}}, "monitor");monitorThread.start();}
它还经常用来实现线程安全的单例
public final class Singleton {private Singleton() {}private static Singleton INSTANCE = null;public static synchronized Singleton getInstance() {if (INSTANCE != null) {return INSTANCE;}INSTANCE = new Singleton();return INSTANCE;}
}
有序性
JVM 会在不影响正确性的前提下,可以调整语句的执行顺序,思考下面一段代码
static int i;
static int j;// 在某个线程内执行如下赋值操作
i = ...;
j = ...;
可以看到,至于是先执行 i 还是 先执行 j ,对最终的结果不会产生影响。所以,上面代码真正执行时,既可以是
i = ...;
j = ...;
也可以是
j = ...;
i = ...;
这种特性称之为『指令重排』,多线程下『指令重排』会影响正确性。为什么要有重排指令这项优化呢?从 CPU
执行指令的原理来理解一下吧
指令级并行原理
名词
Clock Cycle Time
主频的概念大家接触的比较多,而 CPU 的 Clock Cycle Time(时钟周期时间),等于主频的倒数,意思是 CPU 能
够识别的最小时间单位,比如说 4G 主频的 CPU 的 Clock Cycle Time 就是 0.25 ns,作为对比,我们墙上挂钟的
Cycle Time 是 1s
例如,运行一条加法指令一般需要一个时钟周期时间
CPI
有的指令需要更多的时钟周期时间,所以引出了 CPI (Cycles Per Instruction)指令平均时钟周期数
IPC
IPC(Instruction Per Clock Cycle) 即 CPI 的倒数,表示每个时钟周期能够运行的指令数
CPU 执行时间
程序的 CPU 执行时间,即我们前面提到的 user + system 时间,可以用下面的公式来表示
程序 CPU 执行时间 = 指令数 * CPI * Clock Cycle Time
鱼罐头的故事
加工一条鱼需要 50 分钟,只能一条鱼、一条鱼顺序加工…
可以将每个鱼罐头的加工流程细分为 5 个步骤:
- 去鳞清洗 10分钟
- 蒸煮沥水 10分钟
- 加注汤料 10分钟
- 杀菌出锅 10分钟
- 真空封罐 10分钟
即使只有一个工人,最理想的情况是:他能够在 10 分钟内同时做好这 5 件事,因为对第一条鱼的真空装罐,不会
影响对第二条鱼的杀菌出锅…
指令重排序优化
事实上,现代处理器会设计为一个时钟周期完成一条执行时间最长的 CPU 指令。为什么这么做呢?可以想到指令
还可以再划分成一个个更小的阶段,例如,每条指令都可以分为: 取指令 - 指令译码 - 执行指令 - 内存访问 - 数据
写回 这 5 个阶段
在不改变程序结果的前提下,这些指令的各个阶段可以通过重排序和组合来实现指令级并行,这一技术在 80’s 中
叶到 90’s 中叶占据了计算架构的重要地位。(分阶段,分工是提升效率的关键!)
指令重排的前提是,重排指令不能影响结果,例如
// 可以重排的例子
int a = 10; // 指令1
int b = 20; // 指令2
System.out.println( a + b );// 不能重排的例子
int a = 10; // 指令1
int b = a - 5; // 指令2
支持流水线的处理器
现代 CPU 支持多级指令流水线,例如支持同时执行 取指令 - 指令译码 - 执行指令 - 内存访问 - 数据写回 的处理
器,就可以称之为五级指令流水线。这时 CPU 可以在一个时钟周期内,同时运行五条指令的不同阶段(相当于一
条执行时间最长的复杂指令),IPC = 1,本质上,流水线技术并不能缩短单条指令的执行时间,但它变相地提高了指令地吞吐率。(奔腾四(Pentium 4)支持高达 35 级流水线,但由于功耗太高被废弃)
SuperScalar 处理器
大多数处理器包含多个执行单元,并不是所有计算功能都集中在一起,可以再细分为整数运算单元、浮点数运算单
元等,这样可以把多条指令也可以做到并行获取、译码等,CPU 可以在一个时钟周期内,执行多于一条指令,IPC>1
诡异的结果
int num = 0;boolean ready = false;// 线程1 执行此方法public void actor1(I_Result r) {if(ready) {r.r1 = num + num;} else {r.r1 = 1;}}// 线程2 执行此方法public void actor2(I_Result r) {num = 2;ready = true;}
有一个属性 r1 用来保存结果,问,可能的结果有几种?
有同学这么分析
- 情况1:线程1 先执行,这时 ready = false,所以进入 else 分支结果为 1
- 情况2:线程2 先执行 num = 2,但没来得及执行 ready = true,线程1 执行,还是进入 else 分支,结果为1
- 情况3:线程2 执行到 ready = true,线程1 执行,这回进入 if 分支,结果为 4(因为 num 已经执行过了)
但我告诉你,结果还有可能是 0 😁😁😁,信不信吧!
这种情况下是:线程2 执行 ready = true,切换到线程1,进入 if 分支,相加为 0,再切回线程2 执行 num = 2
相信很多人已经晕了 😵😵(原因就是jvm对这 num 和 ready进行了指令重排序,那么结果可能是0)
这种现象叫做指令重排,是 JIT 编译器在运行时的一些优化,这个现象需要通过大量测试才能复现:
借助 java 并发压测工具 jcstress https://wiki.openjdk.java.net/display/CodeTools/jcstress
mvn archetype:generate -DinteractiveMode=false -DarchetypeGroupId=org.openjdk.jcstress -
DarchetypeArtifactId=jcstress-java-test-archetype -DarchetypeVersion=0.5 -DgroupId=cn.itcast -DartifactId=ordering -Dversion=1.0
创建 maven 项目,提供如下测试类
@JCStressTest
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok")
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "!!!!")
@State
public class ConcurrencyTest {int num = 0;boolean ready = false;@Actorpublic void actor1(I_Result r) {if(ready) {r.r1 = num + num;} else {r.r1 = 1;}}@Actorpublic void actor2(I_Result r) {num = 2;ready = true;}
}
执行
mvn clean install
java -jar target/jcstress.jar
会输出我们感兴趣的结果,摘录其中一次结果:
*** INTERESTING testsSome interesting behaviors observed. This is for the plain curiosity.2 matching test results.[OK] test.ConcurrencyTest(JVM args: [-XX:-TieredCompilation])Observed state Occurrences Expectation Interpretation0 1,729 ACCEPTABLE_INTERESTING !!!!1 42,617,915 ACCEPTABLE ok4 5,146,627 ACCEPTABLE ok[OK] test.ConcurrencyTest(JVM args: [])Observed state Occurrences Expectation Interpretation0 1,652 ACCEPTABLE_INTERESTING !!!!1 46,460,657 ACCEPTABLE ok4 4,571,072 ACCEPTABLE ok
可以看到,出现结果为 0 的情况有 一千多次,虽然次数相对很少,但毕竟是出现了。
解决方法
volatile 修饰的变量,可以禁用指令重排
@JCStressTest
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok")
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "!!!!")
@State
public class ConcurrencyTest {int num = 0;volatile boolean ready = false;@Actorpublic void actor1(I_Result r) {if(ready) {r.r1 = num + num;} else {r.r1 = 1;}}@Actorpublic void actor2(I_Result r) {num = 2;ready = true;// 在这上面加 volatile 能够防止 之前的代码被重排序}
}
结果为:
*** INTERESTING tests Some interesting behaviors observed. This is for the plain curiosity. 0 matching test results.
volatile 原理
volatile 的底层实现原理是内存屏障,Memory Barrier(Memory Fence)
- 对 volatile 变量的写指令后会加入写屏障
- 对 volatile 变量的读指令前会加入读屏障
如何保证可见性
写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中
public void actor2(I_Result r) {num = 2;ready = true; // ready 是 volatile 赋值带写屏障// 写屏障}
而读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据
public void actor1(I_Result r) {// 读屏障// ready 是 volatile 读取值带读屏障if(ready) {r.r1 = num + num;} else {r.r1 = 1;}}
如何保证有序性
- 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
- 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
还是那句话,不能解决指令交错:
- 写屏障仅仅是保证之后的读能够读到最新的结果,但不能保证读跑到它前面去
- 而有序性的保证也只是保证了本线程内相关代码不被重排序
volatile底层的读写屏障只是 保证了可见性 和 有序性,还是不能解决指令交错
而synchronized都可以做到,有序 可见 原子
double-checked locking 问题
以著名的 double-checked locking 单例模式为例
public final class Singleton {private Singleton() { }private static Singleton INSTANCE = null;public static Singleton getInstance() {if(INSTANCE == null) { // t2// 首次访问会同步,而之后的使用没有 synchronizedsynchronized(Singleton.class) {if (INSTANCE == null) { // t1INSTANCE = new Singleton();}}}return INSTANCE;}
}
以上的实现特点是:
- 懒惰实例化
- 首次使用 getInstance() 才使用 synchronized 加锁,后续使用时无需加锁
- 有隐含的,但很关键的一点:第一个 if 使用了 INSTANCE 变量,是在同步块之外
但在多线程环境下,上面的代码是有问题的,getInstance 方法对应的字节码为:
0: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
3: ifnonnull 37
6: ldc #3 // class cn/itcast/n5/Singleton
8: dup
9: astore_0
10: monitorenter
11: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
14: ifnonnull 27
17: new #3 // class cn/itcast/n5/Singleton
20: dup
21: invokespecial #4 // Method "<init>":()V
24: putstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
27: aload_0
28: monitorexit
29: goto 37
32: astore_1
33: aload_0
34: monitorexit
35: aload_1
36: athrow
37: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
40: areturn
其中
- 17 表示创建对象,将对象引用入栈 // new Singleton
- 20 表示复制一份对象引用 // 引用地址
- 21 表示利用一个对象引用,调用构造方法
- 24 表示利用一个对象引用,赋值给 static INSTANCE
也许 jvm 会优化为:先执行 24,再执行 21。如果两个线程 t1,t2 按如下时间序列执行:
关键在于 0: getstatic 这行代码在 monitor 控制之外,它就像之前举例中不守规则的人,可以越过 monitor 读取
INSTANCE 变量的值。
这时 t1 还未完全将构造方法执行完毕,如果在构造方法中要执行很多初始化操作,那么 t2 拿到的是将是一个未初
始化完毕的单例。
对 INSTANCE 使用 volatile 修饰即可,可以禁用指令重排,但要注意在 JDK 5 以上的版本的 volatile 才会真正有效
这里面,前面介绍的synchronized不是太严谨 。能保证 原子 可见 有序
synchronized仍然是可以被重排序的,并不能组织重排序,volatile才能组织重排序,但是如果共享变量完全被synchronized 所保护,那么共享变量在使用的过程中是不会有 原子 可见 有序问题的,就算中间发生了重排序,但是只要完全交给synchronized管理,是不会有有序性问题的。
刚才使用出现问题,是因为共享变量并没有完全的被synchronized保护起来,synchronized外面还有共享变量的使用
double-checked locking 解决
public final class Singleton {private Singleton() { }private static volatile Singleton INSTANCE = null;public static Singleton getInstance() {// 实例没创建,才会进入内部的 synchronized代码块if (INSTANCE == null) {synchronized (Singleton.class) { // t2// 也许有其它线程已经创建实例,所以再判断一次if (INSTANCE == null) { // t1INSTANCE = new Singleton();}}}return INSTANCE;}
}
字节码上看不出来 volatile 指令的效果
如上面的注释内容所示,读写 volatile 变量时会加入内存屏障(Memory Barrier(Memory Fence)),保证下面两点:
可见性
写屏障(sfence)保证在该屏障之前的 t1 对共享变量的改动,都同步到主存当中
而读屏障(lfence)保证在该屏障之后 t2 对共享变量的读取,加载的是主存中最新数据
有序性
写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
happens-before
happens-before 规定了对共享变量的写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结,抛
开以下 happens-before 规则,JMM 并不能保证一个线程对共享变量的写,对于其它线程对该共享变量的读可见
线程解锁 m 之前对变量的写,对于接下来对 m 加锁的其它线程对该变量的读可见
static int x;static Object m = new Object();new Thread(()->{synchronized(m) {x = 10;}},"t1").start();new Thread(()->{synchronized(m) {System.out.println(x);}},"t2").start();
线程对 volatile 变量的写,对接下来其它线程对该变量的读可见
volatile static int x;new Thread(()->{x = 10;
},"t1").start();new Thread(()->{System.out.println(x);
},"t2").start();
线程 start 前对变量的写,对该线程开始后对该变量的读可见
static int x;x = 10;new Thread(()->{System.out.println(x);
},"t2").start();
线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用 t1.isAlive() 或 t1.join()等待它结束)
static int x;Thread t1 = new Thread(()->{x = 10;
},"t1");t1.start();
t1.join();
System.out.println(x);
线程 t1 打断 t2(interrupt)前对变量的写,对于其他线程得知 t2 被打断后对变量的读可见(通过t2.interrupted 或 t2.isInterrupted)
Thread t2 = new Thread(()->{while(true) {if(Thread.currentThread().isInterrupted()) {System.out.println(x);break;}}},"t2");t2.start();new Thread(()->{try {Thread.sleep(10000);} catch (InterruptedException e) {e.printStackTrace();}x = 10;t2.interrupt();},"t1").start();
具有传递性,如果 x hb-> y 并且 y hb-> z 那么有 x hb-> z ,配合 volatile 的防指令重排,有下面的例子
volatile static int x;
static int y;
new Thread(()->{ y = 10;x = 20;
},"t1").start();
new Thread(()->{// x=20 对 t2 可见, 同时 y=10 也对 t2 可见System.out.println(x);
},"t2").start();
主要是 写屏障之前都同步到主内存
balking 模式习题
希望 doInit() 方法仅被调用一次,下面的实现是否有问题,为什么?
public class TestVolatile {volatile boolean initialized = false;void init() {if (initialized) {return;}doInit();initialized = true;}private void doInit() {}
}
其实这里,在多线程的情况下,因为没有保证多线程之间的原子性,所以会出现问题。
线程安全单例习题
单例模式有很多实现方法,饿汉、懒汉、静态内部类、枚举类,试分析每种实现下获取单例对象(即调用
getInstance)时的线程安全,并思考注释中的问题
- 饿汉式:类加载就会导致该单实例对象被创建
- 懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建
实现1
// 问题1:为什么加 final
// 问题2:如果实现了序列化接口, 还要做什么来防止反序列化破坏单例
public final class Singleton implements Serializable {// 问题3:为什么设置为私有? 是否能防止反射创建新的实例?private Singleton() {}// 问题4:这样初始化是否能保证单例对象创建时的线程安全?private static final Singleton INSTANCE = new Singleton();// 问题5:为什么提供静态方法而不是直接将 INSTANCE 设置为 public, 说出你知道的理由public static Singleton getInstance() {return INSTANCE;}public Object readResolve() {return INSTANCE;}
}
问题1:怕将来有子类,覆盖某些方法,破坏了单例
问题2:反序列化维护的对象,和单例维护的对象不一样,因为 需要加上
public Obejct readResolve(){
return INSTANCE;
}
反序列化的过程中,一旦发现了readResolve 返回的对象,就会用你返回的对象,而不是反序列化字节码生成的对象。
问题3: 设置成public,别的类能够无限的创建对象了,并不能够防止反射
问题4: 静态成员变量,初始化操作是在类加载的时候。类加载阶段是由jvm来保证这些代码的线程安全性。所以类加载阶段做成员赋值都是线程安全的。
问题5: 用方法说明提供了更好的封装性,可以内部实现懒惰的初始化。还可以对创建的时间有更多的控制。
实现2(※※※重点难点※※※)
// 问题1:枚举单例是如何限制实例个数的
// 问题2:枚举单例在创建时是否有并发问题
// 问题3:枚举单例能否被反射破坏单例
// 问题4:枚举单例能否被反序列化破坏单例
// 问题5:枚举单例属于懒汉式还是饿汉式
// 问题6:枚举单例如果希望加入一些单例创建时的初始化逻辑该如何做
enum Singleton {INSTANCE;
}
问题1: 枚举单例是一种实现单例模式的方式,它通过使用枚举类型来限制实例个数为一个。在Java中,枚举类型是保证全局唯一的,因此使用枚举来实现单例可以有效地避免多线程环境下的并发问题,并且在序列化和反序列化中也可以得到保证。
问题2: 是静态成员变量,类加载阶段完成的,不会有并发问题
问题3: 枚举单例是一种实现单例模式的有效方式,因为它可以避免通过反射和序列化等方式破坏单例。枚举类型在Java中是天然的单例,只能在JVM中被实例化一次。当使用反射来访问枚举类型时,JVM会保证每个枚举类型只被实例化一次。这是因为枚举类型在Java中是特殊的类,由JVM特别处理。因此,即使使用反射来访问枚举类型并试图创建一个新的实例,JVM也会返回已经存在的单例。因此,枚举单例不能被反射破坏单例。
问题4: 枚举单例在Java中也可以防止被反序列化破坏单例。当一个枚举类型被序列化并再次反序列化时,JVM会自动确保只存在一个实例。这是因为枚举类型的序列化和反序列化是由JVM处理的,并且JVM会保证对于同一个枚举类型只有一个实例。因此,尝试通过反序列化来破坏枚举单例是无效的,反序列化操作会返回已经存在的单例实例,而不会创建新的实例。总之,枚举单例是一种安全且可靠的单例实现方式,即使在面对反射和序列化等特性时也能保持单例的完整性。
问题5: 饿汉式
问题6: 如果希望在枚举单例创建时加入一些初始化逻辑,可以在枚举中定义一个构造函数,并将初始化逻辑放在其中
public enum SingletonEnum {INSTANCE;private SingletonEnum() {// 这里可以加入单例创建时的初始化逻辑System.out.println("SingletonEnum has been initialized.");}// 其它方法
}
在上面的示例中,我们定义了一个名为SingletonEnum的枚举,其中INSTANCE是该枚举的唯一实例。同时,我们定义了一个构造函数来进行单例的初始化逻辑。
此处需要注意,在枚举类型中,构造函数必须是私有的。这是因为Java语言规范规定,只能在枚举类型内部定义枚举常量,而枚举常量的创建是由编译器自动生成的。因此,枚举类型的构造函数必须是私有的,以确保只有编译器才能调用它。
实现3:
public final class Singleton {private Singleton() { }private static Singleton INSTANCE = null;// 分析这里的线程安全, 并说明有什么缺点public static synchronized Singleton getInstance() {if( INSTANCE != null ){return INSTANCE;}INSTANCE = new Singleton();return INSTANCE;}
}
性能问题:
由于synchronized关键字锁住的是整个方法,在多线程高并发的情况下,可能会导致性能下降。每次调用getInstance()时都需要获取锁,即使实例已经被创建。
实例提前创建:在多线程环境下,如果有一个线程获取锁并创建了实例,其它线程进入方法后会发现实例已被创建,但仍需要等待锁的释放才能继续执行。这样就造成了实例的提前创建,占用了内存资源。
实现4:DCL
public final class Singleton {private Singleton() { }// 问题1:解释为什么要加 volatile ?private static volatile Singleton INSTANCE = null;// 问题2:对比实现3, 说出这样做的意义 public static Singleton getInstance() {if (INSTANCE != null) {return INSTANCE;}synchronized (Singleton.class) {// 问题3:为什么还要在这里加为空判断, 之前不是判断过了吗if (INSTANCE != null) { // t2 return INSTANCE;}INSTANCE = new Singleton();return INSTANCE;}}
}
问题1: 保障了可见性 和 有序性
可见性
- 写屏障(sfence)保证在该屏障之前的 t1 对共享变量的改动,都同步到主存当中
- 而读屏障(lfence)保证在该屏障之后 t2 对共享变量的读取,加载的是主存中最新数据
有序性
- 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
- 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
问题2: 这种方式在第一次检查时可以避免不必要的锁竞争,提高了性能。并且通过使用volatile关键字修饰实例变量,保证了线程之间对实例的可见性,确保正确的初始化。这样既满足了线程安全性,又提高了性能。
问题3: 其实是为了防止最一开始同时有多个线程 绕过了第一个 不等于 null的判断
实现5
public final class Singleton {private Singleton() { }// 问题1:属于懒汉式还是饿汉式private static class LazyHolder {static final Singleton INSTANCE = new Singleton();}// 问题2:在创建时是否有并发问题public static Singleton getInstance() {return LazyHolder.INSTANCE;}
}
这段代码使用了静态内部类的方式实现了单例模式,常被称为“静态内部类单例模式”。
属于懒汉式还是饿汉式?
该实现方式属于懒汉式,因为实例对象的创建发生在静态内部类LazyHolder被调用时。而不是在类加载时就创建实例对象。
在创建时是否有并发问题?
由于静态内部类LazyHolder在类加载时并不会被初始化,只有当getInstance()方法被调用时才会加载,因此不存在并发创建实例的问题。
同时,由于Java虚拟机在加载类时会对类进行加锁,所以多线程同时加载Singleton类也不会导致并发问题。
这种实现方式同时具备了懒汉式和饿汉式的优点。在需要使用实例对象时才会进行实例化,避免了饿汉式可能造成的资源浪费;同时在加载静态内部类时,JVM会自动加锁,保证了线程安全性。因此,该实现方式既满足了线程安全性,又提高了性能和资源利用率。
相关文章:

剑指JUC原理-8.Java内存模型
👏作者简介:大家好,我是爱吃芝士的土豆倪,24届校招生Java选手,很高兴认识大家📕系列专栏:Spring源码、JUC源码🔥如果感觉博主的文章还不错的话,请👍三连支持&…...

Azure 机器学习 - 使用 AutoML 和 Python 训练物体检测模型
目录 一、Azure环境准备二、计算目标设置三、试验设置四、直观呈现输入数据五、上传数据并创建 MLTable六、配置物体检测试验适用于图像任务的自动超参数扫描 (AutoMode)适用于图像任务的手动超参数扫描作业限制 七、注册和部署模型获取最佳试用版注册模型配置联机终结点创建终…...

【深度学习】pytorch——快速入门
笔记为自我总结整理的学习笔记,若有错误欢迎指出哟~ pytorch快速入门 简介张量(Tensor)操作创建张量向量拷贝张量维度张量加法函数名后面带下划线 _ 的函数索引和切片Tensor和Numpy的数组之间的转换张量(tensor)与标量…...
git本地项目同时推送提交到github和gitee同步
git本地项目同时推送提交到github和gitee同步 同时推送到GitHub和Gitee(码云)可以通过设置多个远程仓库地址来实现。具体步骤如下: 一、分别推送 # 初始化仓库 git init# 添加远程仓库 git remote add gitee gitgitee.com:bealei/test.git…...
结构体数据类型使用的一些注意点
1.结构体定义时的注意事项: 1.错误定义结构体: struct students {char name[9] "Mike";int height 185; }; 这是不对的,在 C 语言中,这是由语言的设计原则所决定的。结构体的定义(struct declaration&…...
Serverless化云产品超40款 阿里云发布全球首款容器计算服务
10月31日,杭州云栖大会上,阿里云宣布推出全球首款容器计算服务ACS,大幅提升操作的易用性并节省20%资源成本,真正将Serverless理念大规模落地,同时阿里云 Serverless化进程进入快车道,有超40款云产品提供了S…...

最小化安装移动云大云操作系统--BCLinux-R8-U2-Server-x86_64-231017版
有个业务系统因为兼容性问题,需要安装el8.2的系统,因此对应安装国产环境下的BCLinuxR8U2系统来满足用户需求。BCLinux-R8-U2-Server是中国移动基于AnolisOS8.2深度定制的企业级X86服务器通用版操作系统。本文记录在DELL PowerEdge R720xd服务器上最小化安…...

索引创建的原则
索引的创建是数据库优化中非常重要的一部分,正确创建索引可以大大提高查询效率。以下是一些创建索引时需要考虑的原则: 根据查询频率创建索引: 频繁用于检索的列: 那些频繁用于查询的列或经常出现在 WHERE、JOIN、ORDER BY 和 GR…...

动态表单生成Demo(Vue+elment)
摘要:本文将介绍如何使用vue和elment ui组件库实现一个简单的动态表单生成的Demo。主要涉及两个.vue文件的书写,一个是动态表单生成的组件文件,一个是使用该动态表单生成的组件。 1.动态表单生成组件 这里仅集成了输入框、选择框、日期框三种…...

JMeter断言之JSON断言
JSON断言 若服务器返回的Response Body为JSON格式的数据,使用JSON断言来判断测试结果是较好的选择。 首先需要根据JSON Path从返回的JSON数据中提取需要判断的实际结果,再设置预期结果,两者进行比较得出断言结果。 下面首先介绍JSON与JSON…...
LuatOS-SOC接口文档(air780E)--mqtt - mqtt客户端
常量 常量 类型 解释 mqtt.STATE_DISCONNECT number mqtt 断开 mqtt.STATE_SCONNECT number mqtt socket连接中 mqtt.STATE_MQTT number mqtt socket已连接 mqtt连接中 mqtt.STATE_READY number mqtt mqtt已连接 mqttc:subscribe(topic, qos) 订阅主题 参数 …...

安装Python环境
Python 安装包下载地址:https://www.python.org/downloads/ 打开该链接,可以看到有两个版本的 Python,分别是 Python 3.x 和 Python 2.x,如下图所示: Python下载页面截图 图 1 Python 下载页面截图(包含…...

[nodejs] 爬虫加入并发限制并发实现痞客邦网页截图
今晚想给偶像的相册截个图,避免某一天网站挂了我想看看回忆都不行,用的是js的木偶师来爬虫台湾的部落格,效果图大概是这样,很不错 问题来了.我很贪心, 我想一次性把相册全爬了,也就是并发 ,这个人的相册有19个!!我一下子要开19个谷歌浏览器那个什么进程, 然后程序就崩了, 我就想…...

GEE——Publisher Data Catalogs发布者数据目录
发布者数据目录 发布者数据目录由数据集发布者策划,供更大范围的 Google 地球引擎社区使用,并作为地球引擎资产集公开共享。这些目录并非由 Google 编制。这里是GEE团队简政放权的一个过程,也就是说这些数据集的后续更新和维护并不由GEE团队负…...
计算10的阶乘
一、不好的写法 public static void main(String[] args) {long fun fun(10);System.out.println(fun);}public static long fun(long n) {if (n 1) {return 1;}return n * fun(n - 1);}使用递归完成需求,fun1方法会执行10次,并且第一次执行未完毕&…...
6.卷积神经网络
#pic_center R 1 R_1 R1 R 2 R^2 R2 目录 知识框架No.1 卷积层一、从全连接到卷积二、卷积层三、代码四、QA No.2 卷积层里的填充和步幅一、填充和步幅二、D2L代码注意点三、QA No.3 卷积层里的多输入和多输出通道一、多输入和多输出通道二、D2L代码注意点三、QA No.4 池化层…...
postgresql|数据库|SQL语句冲突的解决
前言: postgresql数据库是比较复杂的一个关系型数据库,而有些时候,即使是简单的插入更新操作也是有很多复杂的机制。 那么,什么是冲突?什么时候会遇到冲突(也就是冲突的常见场景)?…...

overflow溢出属性、定位、前端基础之JavaScript
overflow溢出属性 值 描述 visible 默认值。内容不会被修剪,会呈现在元素框之外。 hidden 内容会被修剪,并且其余内容是不可见的。 scroll 内容会被修剪,但是浏览器会显示滚动条以便查看其余的内容。 auto 如果内容被修剪࿰…...
【JS】Chapter6-Dom 获取属性操作
站在巨人的肩膀上 黑马程序员前端JavaScript入门到精通全套视频教程,javascript核心进阶ES6语法、API、js高级等基础知识和实战教程 (六)Dom 获取&属性操作 以下的变量可以将 let 改为 const: let arr [red, green] arr.pu…...

太极培训机构展示服务预约小程序的作用如何
太极是适合男女老幼的,很多地方也有相关的学校或培训机构,由于受众广且不太受地域影响,因此对培训机构来说,除了线下经营外,线上宣传、学员获取和发展也不可少。 接下来让我们看下通过【雨科】平台制作太极教培服务预…...

使用docker在3台服务器上搭建基于redis 6.x的一主两从三台均是哨兵模式
一、环境及版本说明 如果服务器已经安装了docker,则忽略此步骤,如果没有安装,则可以按照一下方式安装: 1. 在线安装(有互联网环境): 请看我这篇文章 传送阵>> 点我查看 2. 离线安装(内网环境):请看我这篇文章 传送阵>> 点我查看 说明:假设每台服务器已…...

css实现圆环展示百分比,根据值动态展示所占比例
代码如下 <view class""><view class"circle-chart"><view v-if"!!num" class"pie-item" :style"{background: conic-gradient(var(--one-color) 0%,#E9E6F1 ${num}%),}"></view><view v-else …...

汽车生产虚拟实训中的技能提升与生产优化
在制造业蓬勃发展的大背景下,虚拟教学实训宛如一颗璀璨的新星,正发挥着不可或缺且日益凸显的关键作用,源源不断地为企业的稳健前行与创新发展注入磅礴强大的动力。就以汽车制造企业这一极具代表性的行业主体为例,汽车生产线上各类…...
Qwen3-Embedding-0.6B深度解析:多语言语义检索的轻量级利器
第一章 引言:语义表示的新时代挑战与Qwen3的破局之路 1.1 文本嵌入的核心价值与技术演进 在人工智能领域,文本嵌入技术如同连接自然语言与机器理解的“神经突触”——它将人类语言转化为计算机可计算的语义向量,支撑着搜索引擎、推荐系统、…...
Matlab | matlab常用命令总结
常用命令 一、 基础操作与环境二、 矩阵与数组操作(核心)三、 绘图与可视化四、 编程与控制流五、 符号计算 (Symbolic Math Toolbox)六、 文件与数据 I/O七、 常用函数类别重要提示这是一份 MATLAB 常用命令和功能的总结,涵盖了基础操作、矩阵运算、绘图、编程和文件处理等…...
docker 部署发现spring.profiles.active 问题
报错: org.springframework.boot.context.config.InvalidConfigDataPropertyException: Property spring.profiles.active imported from location class path resource [application-test.yml] is invalid in a profile specific resource [origin: class path re…...

CVE-2020-17519源码分析与漏洞复现(Flink 任意文件读取)
漏洞概览 漏洞名称:Apache Flink REST API 任意文件读取漏洞CVE编号:CVE-2020-17519CVSS评分:7.5影响版本:Apache Flink 1.11.0、1.11.1、1.11.2修复版本:≥ 1.11.3 或 ≥ 1.12.0漏洞类型:路径遍历&#x…...
纯 Java 项目(非 SpringBoot)集成 Mybatis-Plus 和 Mybatis-Plus-Join
纯 Java 项目(非 SpringBoot)集成 Mybatis-Plus 和 Mybatis-Plus-Join 1、依赖1.1、依赖版本1.2、pom.xml 2、代码2.1、SqlSession 构造器2.2、MybatisPlus代码生成器2.3、获取 config.yml 配置2.3.1、config.yml2.3.2、项目配置类 2.4、ftl 模板2.4.1、…...

群晖NAS如何在虚拟机创建飞牛NAS
套件中心下载安装Virtual Machine Manager 创建虚拟机 配置虚拟机 飞牛官网下载 https://iso.liveupdate.fnnas.com/x86_64/trim/fnos-0.9.2-863.iso 群晖NAS如何在虚拟机创建飞牛NAS - 个人信息分享...

break 语句和 continue 语句
break语句和continue语句都具有跳转作用,可以让代码不按既有的顺序执行 break break语句用于跳出代码块或循环 1 2 3 4 5 6 for (var i 0; i < 5; i) { if (i 3){ break; } console.log(i); } continue continue语句用于立即终…...