建设英文网站的必要性,做一个微信小程序游戏需要多少钱,合肥瑶海区范围,韩都衣舍网站建设策划书#x1f44f;作者简介#xff1a;大家好#xff0c;我是爱吃芝士的土豆倪#xff0c;24届校招生Java选手#xff0c;很高兴认识大家#x1f4d5;系列专栏#xff1a;Spring源码、JUC源码#x1f525;如果感觉博主的文章还不错的话#xff0c;请#x1f44d;三连支持作者简介大家好我是爱吃芝士的土豆倪24届校招生Java选手很高兴认识大家系列专栏Spring源码、JUC源码如果感觉博主的文章还不错的话请三连支持一下博主哦博主正在努力完成2023计划中源码溯源一探究竟联系方式nhs19990716加我进群大家一起学习一起进步一起对抗互联网寒冬 文章目录 Java 内存模型可见性退不出的循环解决方法可见性 vs 原子性模式之两阶段终止同步模式之 Balking定义实现 有序性指令级并行原理名词Clock Cycle TimeCPIIPCCPU 执行时间 鱼罐头的故事指令重排序优化支持流水线的处理器SuperScalar 处理器 诡异的结果解决方法 volatile 原理如何保证可见性如何保证有序性double-checked locking 问题double-checked locking 解决可见性有序性 happens-before线程解锁 m 之前对变量的写对于接下来对 m 加锁的其它线程对该变量的读可见线程对 volatile 变量的写对接下来其它线程对该变量的读可见线程 start 前对变量的写对该线程开始后对该变量的读可见线程结束前对变量的写对其它线程得知它结束后的读可见比如其它线程调用 t1.isAlive() 或 t1.join()等待它结束线程 t1 打断 t2interrupt前对变量的写对于其他线程得知 t2 被打断后对变量的读可见通过t2.interrupted 或 t2.isInterrupted具有传递性如果 x hb- y 并且 y hb- z 那么有 x hb- z 配合 volatile 的防指令重排有下面的例子 balking 模式习题线程安全单例习题实现1实现2※※※重点难点※※※实现3实现4DCL实现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的值 线程内i0 getstatic i // 线程1-获取静态变量i的值 线程内i0
iconst_1 // 线程1-准备常量1
iadd // 线程1-自增 线程内i1
putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i1 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
IPCInstruction 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 可以在一个时钟周期内执行多于一条指令IPC1 诡异的结果 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 -DinteractiveModefalse -DarchetypeGroupIdorg.openjdk.jcstress -
DarchetypeArtifactIdjcstress-java-test-archetype -DarchetypeVersion0.5 -DgroupIdcn.itcast -DartifactIdordering -Dversion1.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 BarrierMemory 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 Singleton20 表示复制一份对象引用 // 引用地址21 表示利用一个对象引用调用构造方法24 表示利用一个对象引用赋值给 static INSTANCE
也许 jvm 会优化为先执行 24再执行 21。如果两个线程 t1t2 按如下时间序列执行 关键在于 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 BarrierMemory 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 打断 t2interrupt前对变量的写对于其他线程得知 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(()-{// x20 对 t2 可见, 同时 y10 也对 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()时都需要获取锁即使实例已经被创建。
实例提前创建在多线程环境下如果有一个线程获取锁并创建了实例其它线程进入方法后会发现实例已被创建但仍需要等待锁的释放才能继续执行。这样就造成了实例的提前创建占用了内存资源。
实现4DCL
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会自动加锁保证了线程安全性。因此该实现方式既满足了线程安全性又提高了性能和资源利用率。