前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >深入理解Java内存模型

深入理解Java内存模型

作者头像
早安嵩骏
发布2020-08-11 16:23:24
3620
发布2020-08-11 16:23:24
举报
文章被收录于专栏:程序猿人程序猿人
什么是Java内存模型?

Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model,JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能到一致的内存访问效果。

Java内存模型是个很复杂的规范,从我们程序员的角度来说主要掌握并发过程中,如何确保内存的正确性。内存模型对并发的影响主要是:

  1. 缓存导致的可见性问题;
  2. 线程切换导致的原子性问题;
  3. 编译优化带来的有序性问题;

不管是增加缓存还是jvm进行编译优化,无非都是为了提升性能。所以我们更应合理地使用这些东西,在满足业务需要的同时,保证性能的最大化。Java内存模型为我们提供了volatile、锁和final三个实现,以及Happens-Before规则来帮助我们实现这种最大化。

关键字volatile

关键字volatile是Java虚拟机提供的最轻量级的同步机制。当一个变量被定义为volatile后,它将具备两种特性:

  1. 保证次变量对所有线程的可见性可见性是指当一条线程修改了这个变量的值,新值对于其他线程来说是立即可以得知的。而普通变量不能做到这一点,普通变量的值在线程间传递均需要通过主内存来完成;注意,可见性可以说是相比原子性弱一点的存在,原子性是通过synchronized或者其他加锁手段实现的,它的执行是不会被打断的,能够保证内存操作的准确性,但是可见性不行,从volatile来说,它可以被认为是对CPU缓存的屏蔽,直接对主内存进行读写,所以可以保证当一个线程修改的时候,其他线程读取到的值都是最新的。但是如果涉及到多个线程写操作,则不确保准确。

如图,普通的变量,箭头内的操作可以认为是原子操作,而箭头之间,可以产生并发,而被volatile修饰的变量,2个红色框分别可认为是原子操作,但是终究不是整体的原子操作,假设一个变量V同时被2个线程拉入了执行引擎,并改变了值,然后再执行写入,这时候主内存最终的值就不一定了。

  1. 禁止指令的重排序优化在执行程序时,为了提高速度,编译器和处理器常常会对指令做重排序。指令重排序指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理的策略。举个常见的例子:
代码语言:javascript
复制
/**
 * GLC单例
 */
public class Singleton {

    private volatile static Singleton instance;

    private Singleton() {}
    
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

在这里,关键字volatile发挥的就是它防止指令重排的作用。新建一个对象的new操作并不是一个原子性的操作,它大致可分为:

  1. 分配对象内存;
  2. 调用构造器方法;
  3. 执行初始化将对象引用赋值给变量

如果2和3发生指令重排,那么这个时候如果存在另外一个线程进入这个方法,判断第一道null值检查为false,直接就会返回一个没有新建完全的对象,导致错误;这个例子也说明了另外一个问题,synchronized对代码块的加锁,保证了只有一个线程进入临界区域,所以符合下文要说的happens-before规则的第一条,也进而说明锁操作,是不会禁止指令的重排序优化的。

volatile的使用场景:
  • 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值;
  • 变量不需要与其他的状态变量共同参与不变约束;
关键字final

final关键字正如我们所知道的,被final修饰的变量,一旦被初始化,则不可被修改。则它一旦被初始化(逃逸除外),就天然地对其他线程可见;

同volatile,其实锁的可见性是由“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)”这条规则实现的。

happens-before原则

翻译为”先行发生原则“,这里的先行发生,并不是指代时间上的先后顺序,而是用来描述两个操作的内存可见性的。如果操作X happens-before操作Y,那么X的结果对于Y可见。Happens-Before 约束了编译器的优化行为,虽允许编译器优化,但是要求编译器优化后一定遵守 Happens-Before 规则。

  1. 程序次序规则:在一个线程内,按照程序代码顺序,前面的操作先行发生于后边的操作;这里的代码顺序,其实指代的是一种依赖关系,codeA的实现依赖于codeB的数据结果,那么codeA happens-before codeB;这也是as-if-serial语义所要求的,编译器和处理器为了提高速度,会对指令进行重排序,但无论怎么重排序,单线程的运行结果不能够被改变;
  2. 管程锁定规则:一个unlock操作先行发生于后边对同一个锁的lock操作;
  3. volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作;这是一个写先于读的问题,其实就是为了保证读取数据的Thread读到的都是最新写入的;
  4. 线程启动规则:Thread对象的start()方法先行发生于对此线程的每一个动作;这句话有些晦涩,其实就是表示执行A.start()方法之前的操作,都对线程A可见;
  5. 线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测;在主线程A执行过程中,子线程B终止,那么线程B在终止之前对共享变量的修改结果在线程A中可见。也称线程join()规则
  6. 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生;线程对其他线程的中断操作 happens-before 被中断线程所收到的中断事件(即被中断线程的 InterruptedException 异常,或者第三个线程针对被中断线程的 Thread.interrupted 或者 Thread.isInterrupted 调用)
  7. 对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始;也就是说对象没有完成初始化之前,是不能调finalized()方法的;
  8. 传递性:如果操作A先行发生于操作B,操作B先行发生于操作C,则操作A先行发生于操作C。 参考文章:《深入理解JAVA虚拟机》
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-03-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程序猿人 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 什么是Java内存模型?
  • 关键字volatile
    • volatile的使用场景:
    • 关键字final
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档