1 Java内存模型
Java 内存模型(JMM)JMM 即 Java Memory Model,它定义了主存(共享内存)、工作内存(线程私有)抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、 CPU 指令优化等。
JMM 体现在以下几个方面
- 原子性 - 保证指令不会受到线程上下文切换的影响
- 可见性 - 保证指令不会受 cpu 缓存的影响
-
2 可见性
2.1 退不出的循环
```java @Slf4j(topic = “c.Test1”) public class Test1 { public static boolean run = true;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while(run) {
}
}, "t1");
t1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("t1 Stop");
run = false;
}
}
首先 t1 线程运行,然后过一秒,主线程设置 run 的值为 false,想让 t1 线程停止下来,但是 t1 线程并没有停。<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/12943861/1620562416686-17191d25-54be-4429-a9f3-e54acaf2f8af.png#clientId=uc3afed68-e1c9-4&from=paste&height=735&id=u2aae4500&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1142&originWidth=1043&originalType=binary&ratio=1&size=532080&status=done&style=none&taskId=u21d754d2-fcec-47b9-8e36-594958b0848&width=671.5)<br />即一个线程对主存数据进行修改对于另外一个线程不可见。
<a name="hdZmj"></a>
## 2.2 解决办法
- 使用 volatile (易变关键字)
- 它可以用来修饰成员变量和静态成员变量(放在主存中的变量),他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存
```java
public static volatile boolean run = true; // 保证内存的可见性
2.3 可见性与原子性
上面例子体现的实际就是可见性,它保证的是在多个线程之间,一个线程对volatile 变量的修改对另一个线程可见, 不能保证原子性,仅用在一个写线程,多个读线程的情况。
- 注意 synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是 synchronized 是属于重量级操作,性能相对更低。
- 如果在前面示例的死循环中加入 System.out.println() 会发现即使不加 volatile 修饰符,线程 t 也能正确看到 对 run 变量的修改了,想一想为什么?
因为 printIn() 方法使用了 synchronized 同步代码块,可以保证原子性与可见性,它是 PrintStream 类的方法。
2.4 使用volatile优化两阶段终止模式
```java @Slf4j(topic = “c.Test2”) public class Test2 { public static void main(String[] args) {
TwoPhaseTermination t = new TwoPhaseTermination();
// 启动监控线程
t.start();
// 主线程等3.5秒后
try {
Thread.sleep(3500);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 优雅的停止监控线程
t.stop();
}
}
@Slf4j(topic = “c.TwoPhaseTermination”) class TwoPhaseTermination { private Thread monitor; private volatile boolean stop = false;
// 启动监控线程
public void start(){
monitor = new Thread(()->{
while (true){
Thread current = Thread.currentThread();
if (stop){
log.debug("处理后事");
break;
}
try {
Thread.sleep(1000);
log.debug("监控记录");
} catch (InterruptedException e) {
current.interrupt();
}
}
}, "monitor");
monitor.start();
}
// 停止监控线程
public void stop(){
stop = true;
monitor.interrupt();
}
}
```java
21:32:57.804 [monitor] DEBUG c.TwoPhaseTermination - 监控记录
21:32:58.809 [monitor] DEBUG c.TwoPhaseTermination - 监控记录
21:32:59.814 [monitor] DEBUG c.TwoPhaseTermination - 监控记录
21:33:00.300 [monitor] DEBUG c.TwoPhaseTermination - 处理后事
2.5 犹豫模式 Balking
Balking (犹豫)模式用在一个线程发现另一个线程或本线程已经做了某一件相同的事,那么本线程就无需再做了,直接结束返回,有点类似单例。
- 用一个标记来判断该任务是否已经被执行过了
- 需要避免线程安全问题
加锁的代码块要尽量的小,以保证性能 ```java @Slf4j(topic = “c.Test2”) public class Test2 { public static void main(String[] args) {
TwoPhaseTermination t = new TwoPhaseTermination();
// 启动监控线程
t.start();
t.start();
t.start();
}
}
@Slf4j(topic = “c.TwoPhaseTermination”) class TwoPhaseTermination { private Thread monitor; private volatile boolean stop = false;
// 判断是否执行过start方法
private boolean staring = false;
// 启动监控线程
public void start(){
synchronized (this){
if (staring){
return;
}
// 读写要保证原子性 加锁
staring = true;
}
monitor = new Thread(()->{
while (true){
Thread current = Thread.currentThread();
if (stop){
log.debug("处理后事");
break;
}
try {
Thread.sleep(1000);
log.debug("监控记录");
} catch (InterruptedException e) {
current.interrupt();
}
}
}, "monitor");
monitor.start();
}
// 停止监控线程
public void stop(){
stop = true;
monitor.interrupt();
}
}
```java
21:44:10.638 [monitor] DEBUG c.TwoPhaseTermination - 监控记录
21:44:11.642 [monitor] DEBUG c.TwoPhaseTermination - 监控记录
21:44:12.643 [monitor] DEBUG c.TwoPhaseTermination - 监控记录
21:44:13.646 [monitor] DEBUG c.TwoPhaseTermination - 监控记录
- 应用:
// 不能重排的例子 int a = 10; int b = a - 5;
指令重排简单来说可以,在程序结果不受影响的前提下,可以调整指令语句执行顺序。多线程下指令重排会影响正确性。
<a name="dAAMW"></a>
## 3.2 多线程下指令重排问题
> 代码示例
```java
int num = 0;
// volatile 修饰的变量,可以禁用指令重排 volatile boolean ready = false; 可以防止变量之前的代码被重排序
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 的值有三种情况:
- 第一种:线程 2 先执行,然后线程 1 后执行,r1 的结果为 4
- 第二种:线程 1 先执行,然后线程 2 后执行,r1 的结果为 1
- 第三种:线程 2 先执行,但是发送了指令重排,num = 2 与 ready = true 这两行代码语序发生装换,然后先执行 ready = true 后,线程 1 运行了,那么 r1 的结果是为 0。
- 使用相关工具包可以看出,在测试几千万次才有5404次发生了指令重排,而得到了结果0。
3.3 解决办法
volatile 修饰的变量,可以禁用指令重排,禁止的是加 volatile 关键字变量之前的代码重排序。
所以此例可以将volatile加到ready变量上面。
4 volatile原理
volatile 的底层实现原理是内存屏障,Memory Barrier(Memory Fence)
写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中
public void actor2(I_Result r) {
num = 2;
ready = true; // ready 是被 volatile 修饰的,赋值带写屏障
// 写屏障,将之前的num、ready共享变量同步到主存中
}
读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据
public void actor1(I_Result r) {
// 读屏障
// ready是被 volatile 修饰的,读取值带读屏障,读取的是ready在主存中的数据,而不是自己内存中缓存的数据
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
4.2 保证有序性
写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后 ```java public void actor2(I_Result r) { num = 2; ready = true; // ready 是被 volatile 修饰的,赋值带写屏障 // 写屏障 }
- 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
```java
public void actor1(I_Result r) {
// 读屏障
// ready 是被 volatile 修饰的,读取值带读屏障
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
注意:volatile 不能解决指令交错(原子性)。写屏障仅仅是保证之后的读能够读到最新的结果,但不能保证其它线程的读跑到它前面去。而有序性的保证也只是保证了本线程内相关代码不被重排序。(图中不能保证t2线程读取i的值会在t1线程写入i的值之后)synchronized可以三个都保证
5. 单例模式应用
懒汉式单例模式的改进
- 版本1:
```java
// 最开始的单例模式是这样的
public final class Singleton {
}private Singleton() { }
private static Singleton INSTANCE = null;
public static Singleton getInstance() {
// 首次访问会同步,而之后的使用不用进入synchronized
synchronized(Singleton.class) {
if (INSTANCE == null) { // t1
INSTANCE = new Singleton();
}
}
return INSTANCE;
}
效率是有问题的,因为即使已经产生了单实例之后,之后调用了getInstance()方法之后还是会加锁,这会严重影响性能!因此就有了模式如下double-checked lockin:
- 版本2:
```java
public final class Singleton {
private Singleton() { }
private static Singleton INSTANCE = null;
public static Singleton getInstance() {
if(INSTANCE == null) { // t2
// 首次访问会同步,而之后的使用没有 synchronized
synchronized(Singleton.class) {
if (INSTANCE == null) { // t1
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
以上的实现特点是:
- 懒惰实例化
- 首次使用 getInstance() 才使用 synchronized 加锁,后续使用时无需加锁
- 有隐含的,但很关键的一点:第一个 if 使用了 INSTANCE 变量,是在同步块之外。
但在多线程环境下,上面的代码是有问题的,getInstance 方法对应的字节码为:
0: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
3: ifnonnull 37
// ldc是获得类对象
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 按如下时间序列执行:
由于 INSTANCE = new Singleton(); 不是原子操作,分为两步,调用构造方法初始化该对象(字节码21);为INSTANCE 对象赋值(字节码24)。t1线程在执行synchronized代码块时,如果发生指令重排,即先赋值后初始化。因为赋值完成后INSTANCE 就不为空了,而synchronized外面的判断(代码第5行)不受保护,此时t2线程在对象初始化之前、赋值之后执行了该判断,就拿到了一个没有初始化完毕的单例对象(半成品),直接会返回,使用对象会出现错误。
注:
- synchronized不能阻止内部(指令)发生重排序的,synchronized有序性指的是多个线程之间的有序性,线程与线程间,每一个 synchronized 块可以看成是一个原子操作。块与块之间是原子操作,块与块之间有序可见。
- synchronized执行完会把所有变量的最新值刷新进主存中,在执行这块代码块时是单线程执行的,只要变量完全受他保护,外部拿到后是没问题的。synchronized保证了代码块的单线程执行,单线程会存在指令重排的过程,其遵循as-if-serial原则,即无论怎么重排,都不会影响最终结果。块中的非原子操作依然会发生指令重排,这会影响到synchronized代码块外面的结果。
- 由于instance并没有完全受到synchronized保护,他不能保证synchronized外面代码的运行情况,所以在外部获取值的时候会受到里面重排序的影响。即在线程1内部发生了指令重排会影响线程2对instance的判断情况,如果此时线程1赋值操作优先于对象初始化,线程2此时判断到instance不为null,而执行下面的过程就会拿到半成品从而导致错误。
- 解决办法:加volatile关键字,防止指令重排
```java
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) { // t1
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
如上面的注释内容所示,读写 volatile 变量操作(即 getstatic 操作和 putstatic 操作)时会加入内存屏障(Memory Barrier(Memory Fence)),保证下面两点:
1. 可见性
1. 写屏障(sfence)保证在该屏障之前的 t1 对共享变量的改动,都同步到主存当中。保证主存中的instance为初始化好的对象。
1. 而读屏障(lfence)保证在该屏障之后 t2 对共享变量的读取,加载的是主存中最新数据(t2要么读取已经初始化好的对象(成品),要么读取null,进入synchronized代码块)
2. 有序性
1. 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后。即在赋值之前,写屏障保证实例已经被初始化了。
1. 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
3. 更底层是读写变量时使用 lock 指令来多核 CPU 之间的可见性与有序性
![截屏2021-05-10 下午12.25.40.png](https://cdn.nlark.com/yuque/0/2021/png/12943861/1620620745305-49de32c3-9ef5-4ec9-a5d2-cbdaf9727ce6.png#clientId=uc3afed68-e1c9-4&from=drop&id=u2a427fec&margin=%5Bobject%20Object%5D&name=%E6%88%AA%E5%B1%8F2021-05-10%20%E4%B8%8B%E5%8D%8812.25.40.png&originHeight=770&originWidth=868&originalType=binary&ratio=1&size=119390&status=done&style=none&taskId=u62776d98-a10b-49c8-b7b7-8942fea5e3c)
<a name="LWx8W"></a>
# 6. happens-before
下面说的变量都是指成员变量或静态成员变量
- 线程解锁 m 之前对变量的写,对于接下来对 m 加锁的其它线程对该变量的读可见
```java
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 变量的写,对接下来其它线程对该变量的读可见
```java
volatile static int x;
new Thread(()->{
},”t1”).start(); new Thread(()->{x = 10;
},”t2”).start();System.out.println(x);
- 线程 start 前对变量的写,对该线程开始后对该变量的读可见
```java
static int x;
x = 10;
new Thread(()->{
System.out.println(x);
},"t2").start();
- 线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用 t1.isAlive() 或 t1.join()等待它结束) ```java 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)
```java
static int x;
public static void main(String[] args) {
Thread t2 = new Thread(()->{
while(true) {
if(Thread.currentThread().isInterrupted()) {
System.out.println(x);
break;
}
}
},"t2");
t2.start();
new Thread(()->{
sleep(1);
x = 10;
t2.interrupt();
},"t1").start();
while(!t2.isInterrupted()) {
Thread.yield();
}
System.out.println(x);
}
- 对变量默认值(0,false,null)的写,对其它线程对该变量的读可见
- 具有传递性,如果 x hb-> y 并且 y hb-> z 那么有 x hb-> z ,配合 volatile 的防指令重排,有下面的例子(即写屏障之前的所有操作都同步到主存)
```java
volatile static int x;
static int y;
new Thread(() -> {
},”t1”).start();y = 10;
x = 20;
},”t2”).start();new Thread(() -> {
// x=20 对 t2 可见, 同时 y=10 也对 t2 可见
System.out.println(x);
<a name="M4LPF"></a>
# 7. 再看单例模式
**线程安全单例习题:**<br />单例模式有很多实现方法,饿汉、懒汉、静态内部类、枚举类,试着分析每种实现下获取单例对象(即调用 getInstance)时的线程安全,并思考注释中的问题
- 饿汉式:类加载就会导致该单实例对象被创建
- 懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建
> 实现1:饿汉式
```java
// 问题1:为什么加 final?防止子类继承后更改
// 问题2:如果实现了序列化接口, 还要做什么来防止反序列化破坏单例,如果进行反序列化的时候会生成新的对象,这样跟单例模式生成的对象是不同的。要解决直接加上readResolve()方法就行了,如下所示
public final class Singleton implements Serializable {
// 问题3:为什么设置为私有? 放弃其它类中使用new生成新的实例。是否能防止反射创建新的实例?不能。
private Singleton() {}
// 问题4:这样初始化是否能保证单例对象创建时的线程安全?保证线程安全,这是类变量,是jvm在类加载阶段就进行了初始化,jvm保证了此操作的线程安全性
private static final Singleton INSTANCE = new Singleton();
// 问题5:为什么提供静态方法而不是直接将 INSTANCE 设置为 public, 说出你知道的理由。
//1.提供更好的封装性,内部实现懒惰初始化;2.提供范型的支持
public static Singleton getInstance() {
return INSTANCE;
}
// 问题2解决方式:实现该方法后反序列化后的对象是你返回的对象,而不是反序列化产生的新对象
public Object readResolve() {
return INSTANCE;
}
}
实现2: 饿汉式
// 问题1:枚举单例是如何限制实例个数的:创建枚举类的时候就已经定义好了,每个枚举常量其实就是枚举类的一个静态成员变量,单实例的。
// 问题2:枚举单例在创建时是否有并发问题:没有,这是静态成员变量。在类加载的时候初始化,jvm会保证线程安全。
// 问题3:枚举单例能否被反射破坏单例:不能
// 问题4:枚举单例能否被反序列化破坏单例:枚举类默认实现了序列化接口,枚举类已经考虑到此问题,无需担心破坏单例
// 问题5:枚举单例属于懒汉式还是饿汉式:饿汉式
// 问题6:枚举单例如果希望加入一些单例创建时的初始化逻辑该如何做:加构造方法就行了
enum Singleton {
INSTANCE;
}
实现3:懒汉式
public final class Singleton {
private Singleton() { }
private static Singleton INSTANCE = null;
// 分析这里的线程安全, 并说明有什么缺点:synchronized加在静态方法上,可以保证线程安全。缺点就是锁的范围过大,每次访问都会加锁,性能比较低。
public static synchronized Singleton getInstance() {
if( INSTANCE != null ){
return INSTANCE;
}
INSTANCE = new Singleton();
return INSTANCE;
}
}
实现4:DCL懒汉式(具体参见第5小节)
public final class Singleton {
private Singleton() { }
// 问题1:解释为什么要加 volatile ?为了防止synchronized的重排序问题
private static volatile Singleton INSTANCE = null;
// 问题2:对比实现3, 说出这样做的意义:提高了效率
public static Singleton getInstance() {
if (INSTANCE != null) {
return INSTANCE;
}
synchronized (Singleton.class) {
// 问题3:为什么还要在这里加为空判断, 之前不是判断过了吗?这是为了别的线程并发访问会重复创建INSTANCE的问题
if (INSTANCE != null) { // t2
return INSTANCE;
}
INSTANCE = new Singleton();
return INSTANCE;
}
}
}
另一种写法
public final class Singleton {
private Singleton(){}
private static volatile Singleton INSTANCE = null;
public static Singleton getInstance(){
if(INSTANCE == null){
synchronized(Singleton.class){
if(INSTANCE == null){
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
实现5:静态内部类懒汉式
public final class Singleton implements Serializable{
private Singleton() { }
// 问题1:属于懒汉式还是饿汉式:懒汉式,这是一个静态内部类。类加载本身就是懒惰的,在没有调用getInstance方法时是没有执行LazyHolder内部类的类加载操作的。
private static class LazyHolder {
private static final Singleton INSTANCE = new Singleton();
}
// 问题2:在创建时是否有并发问题,这是线程安全的,类加载时,jvm保证类加载操作的线程安全
public static Singleton getInstance() {
return LazyHolder.INSTANCE;
}
// 解决反序列化问题
public Object readResolve() {
return getInstance();
}
}
8.小结
本章重点讲解了 JMM 中的
- 可见性 - 由 JVM 缓存优化引起
- 有序性 - 由 JVM 指令重排序优化引起
- happens-before 规则
- 原理方面
- volatile
- 模式方面
- 两阶段终止模式的 volatile 改进
- 同步模式之 balking