不能不了解的JMM & Volatile

JMM & Volatile

在这里插入图片描述

前面说了在没有JMM下,多线程情况时会有一些问题,那么JMM是怎么解决这些问题的呢?

没有看JMM的可以看一下前面的文章,这是链接:Java内存模型

volatile的可见性

一般情况下,对于共享变量的操作都是加锁实现,Java中提供了Volatile关键字确保其他线程能看到共享变量的改变,使得变量的保持一致性的。

Volatile做了两件事情:

  1. 将当前处理器缓存行的数据写回到系统内存
  2. 一个处理器的缓存回写到内存会导致其他处理器的缓存无效

在高速缓存架构下,处理器不直接和主内存通信,为了保证各个处理器缓存是一致的,就诞生了缓存一致性协议mesi,每个处理器通过嗅探在总线上传播的数据来检查自己的缓存是否过期。

在这里插入图片描述

对于Volatile做的第一件事:在以前会直接锁总线,开销比较大,现在,是通过锁缓存,通过锁定这块内存区域的缓存并会写到内存,并使用缓存一致性机制来确保修改的原子性。(这为缓存锁定)

对于第二件事:处理器使用嗅探机制去保证数据的一致性,如果与总线上数据不一致,使其缓存行无效。

总得来说,volatile其实就是MESI协议的实现。

MESI只是使缓存的数据达到一致性效果,

Happens-before 重点是解决前一个操作结果对后一个操作可见,下面有详细的解释。

volatile防止指令重排

先看一个非常典型的禁止重排优化的例子DCL,如下:

public class DoubleCheckLock {
    private static DoubleCheckLock instance;

    private DoubleCheckLock() {
    }

    public static DoubleCheckLock getInstance() { //第一次检测
        if (instance == null) {
            //同步
            synchronized (DoubleCheckLock.class) {
                if (instance == null) {
                    //多线程环境下可能会出现问题的地方 
                    instance = new DoubleCheckLock(); 
                }
            }
        }
        return instance;
    }
}
上述代码一个经典的单例的双重检测的代码,这段代码在单线程环境下并没有什么问
题,但如果在多线程环境下就可以出现线程安全问题。原因在于某一个线程执行到第一次检

测,读取到的instance不为null时,instance的引用对象可能没有完成初始化。 因为instance = new DoubleCheckLock();可以分为以下3步完成(伪代码)

memory = allocate();//1.分配对象内存空间 instance(memory);//2.初始化对象
instance = memory;//3.设置instance指向刚分配的内存地址,此时instance!=null

由于步骤1和步骤2间可能会重排序,如下:

memory = allocate() //分配对象内存空间
2 instance=memory;//3.设置instance指向刚分配的内存地址,此时instance!=null,但是对象还没有初始化完成!
instance(memory);2.初始化对象

由于步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果 在单线程中并没有改变,因此这种重排优化是允许的。但是指令重排只会保证串行语义的执 行的一致性(单线程),但并不会关心多线程间的语义一致性。所以当一条线程访问instance 不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题。那么该如何 解决呢,很简单,我们使用volatile禁止instance变量被执行指令重排优化即可。

//禁止指令重排优化
private volatile static DoubleCheckLock instance;

volatile关键字一个作用就是禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象,关于指令重排优化前面已详细分析过,这里主要简单说明一下volatile是如 何实现禁止指令重排优化的。

简单的说是通过内存屏障在指令间插入一条Memory Barrier则会告诉 编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插 入内存屏障禁止在内存屏障前后的指令执行重排序优化。(具体的内存屏障网上有很多资料)

volatile无法保证原子性

如下即使volatile修饰也是如此

x = 10;   //原子性
y = x;    //变量之间的赋值,不是原子性
x++;      //对变量进行复合操作,不是原子性
x = x + 1 // 同x++一样,不是原子性

必须使用synchronized修饰,以便保证线程安全,需要注意的是一旦使用 synchronized修饰方法后,由于synchronized本身也具备与volatile相同的特性,即可见 性,因此在这样种情况下就完全可以省去volatile修饰变量。

JMM的关键

as-if-serial 语义

在单线程下,as-if-serial 语义:不管怎么重排序,单线程程序的执行结果不能被改变。编译器和处理器都要遵从as-if-serial语义。因此对于数据存在依赖关系时,不会进行重排序()。没有依赖关系就会重排序(如2,3,4行)。

int x,y; // 1
x = 1;   // 2
y = 1;   // 3
x = x + 1;// 4

int a = 1; // 5
int b = 1; // 6
int c = a * b;//7

在这里插入图片描述

happens-before

但是对于多线程来说,重排序可能改变程序执行的结果,如上篇的代码实操的链接:Java内存模型

只靠sychronized和volatile关键字来保证原子性、可见性以及有序性,那么编写并发 程序可能会显得十分麻烦,幸运的是,从JDK 5开始,Java使用新的JSR-133内存模型,提 供了happens-before 原则来辅助保证程序执行的原子性、可见性以及有序性的问题,它是 判断数据是否存在竞争、线程是否安全的依据,happens-before 原则内容如下

  1. 程序顺序原则,即在一个线程内必须保证语义串行性,也就是说按照代码顺序执行。
  2. 锁规则 解锁(unlock)操作必然发生在后续的同一个锁的加锁(lock)之前,也就是 说,如果对于一个锁解锁后,再加锁,那么加锁的动作必须在解锁动作之后(同一个锁)。
  3. volatile规则 volatile变量的写,先发生于读,这保证了volatile变量的可见性,简 单的理解就是,volatile变量在每次被线程访问时,都强迫从主内存中读该变量的 值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存,任何时刻,不同的 线程总是能够看到该变量的最新值。
  4. 线程启动规则 线程的start()方法先于它的每一个动作,即如果线程A在执行线程B 的start方法之前修改了共享变量的值,那么当线程B执行start方法时,线程A对共享 变量的修改对线程B可见
  5. 传递性 A先于B ,B先于C 那么A必然先于C
  6. 线程终止规则 线程的所有操作先于线程的终结,Thread.join()方法的作用是等待 当前执行的线程终止。假设在线程B终止之前,修改了共享变量,线程A从线程B的 join方法成功返回后,线程B对共享变量的修改将对线程A可见。
  7. 线程中断规则 对线程 interrupt()方法的调用先行发生于被中断线程的代码检测到 中断事件的发生,可以通过Thread.interrupted()方法检测线程是否中断。
  8. 对象终结规则对象的构造函数执行,结束先于finalize()方法
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值