专栏首页码云大作战Java内存模型 - JMM

Java内存模型 - JMM

Java语言的其中一个特点为跨平台性即由Java编写的程序,一次编译后就可以在多个系统平台上运行。

正式Java虚拟机中存在JMM(Java Memory Mode),才可以实现让Java达到一次编译,处处运行的效果。

一、硬件内存结构

由于计算机中的存储内存与处理器的运算速度有这量级的差距,因此计算机不得不在主内存和处理器之间加入高速缓存来作为存储内存与处理器之间的缓冲区域。高速缓存的读写速度接近于处理器的运算速度,因此在实际的程序运算中,会将运算需要用到的数据从主内存中复制到缓存中,处理器读取缓存中的数据即可进行运算,运算结束后高速缓存再讲运算结果同步到主内存中,完成计算机中的运算和读取操作。

硬件内存结构如下图,可以看到每个处理器都有自己的高速缓存,那么在这些缓存同步到主内存时会导致缓存之间的数据不一致性。为了解决数据不一致性,在处理器执行操作时会使用MSI、MESI、MOSI等协议来保证他们之间的缓存一致性(在Java虚拟机中也会有类似的问题,一般是使用关键字和happens-before原则来保证一致性)。

为了保证处理器运算效率和保证处理器内部单元可以被充分利用,处理器会对输入的程序进行乱序排序优化,在计算完成后再重组结果(在Java中也会有类似的重排策略)。

二、JMM

为了保证JMM可以屏蔽硬件中内存和处理器的差异,所以可以发现Java的内存模型与硬件的内存结构是类似的,在JMM中也存在主内存、工作内存(高速缓存)、处理器线程(处理器)。并且在JMM中也存在重排策略,并且比硬件中的重排策略优化性更高、更加细致。

Java内存模型结构如下图:

JMM与硬件内存结构相似,他规定了所有的变量都存储在主内存中,所有的运算处理都在线程中完成,并且每条线程都有自己的工作内存,在工作时每个工作内存保存了线程所需要用到的变量的拷贝。

三、JMM的实现

JMM的实现是为了保证在并发情况下保证线程的原子性、可见性、有序性。JMM提供了一系列的关键字(synchronized、volatile、final等)来保证。

· 原子性

原子性表示线程的操作不可中断,要么全部成功,要么全部失败。在JMM中使用了关键字synchronized来保证线程操作的原子性。

synchronized关键字后面会专门作为知识点来写,简单来说就是在虚拟机中synchronized是根据指令来执行的,底层的指令分为monitorer和monitorexit,当线程遇到monitorer指令时线程会尝试回去锁,那么锁计数器会+1.如果没有锁就会被阻塞。当线程遇到monitorexit指令时,锁计数器-1,当计数器为0时,表示同步块执行,释放锁资源。但是如果连续遇到了2次monitorexit时表示线程是遇到了异常而释放的锁。

· 可见性

可见性表示一个线程修改了变量值后可立即将新值同步到共享内存中。在JMM中提供了关键字volatile、synchronized来保证线程操作的可见性。

volatile关键字已经专门作为知识点来写了,如果想要了解可以查看历史文章记录。简单来说是因为被volatile关键字修饰的变量可以保证该变量被修改后被立马同步到主内存中,且每次使用时都刷新该变量的值。普通变量由于需要经过工作内存到主内存的同步,因此普通变量的修改无法保证可见性。

synchronized关键字可以保证可见性是因为在同步块中只有获得锁资源的线程才可以执行方法,保证每次都会把变量修改同步到主内存中。

· 有序性

有序性表示在线程中所有的操作都是有序的。但是在多线程环境下,重排策略会导致在多线程中是无序的。JMM中提供了关键字volatile、synchronized关键字来保证线程的有序性。

volatile关键字是因为被volatile修饰的变量被修改后会立马同步到主内存中,所以他本身包含了禁止重排策略的语义。

synchronized关键字是因为在该同步块中只有获得锁的线程才能执行,当多线程情况下,可以保证被程序串行的来运行。

· JMM重排策略

(1)编译器重排:编译器在不改版程序语义的情况下,会重新安排语句的执行顺序。

(2)指令并行重排:在不存在语句依赖关系的情况下,处理器会重新安排语句的执行顺序。

(3)内存系统的重排:处理器和主内存中由于工作内存的存在会导致缓存与内存的同步存在时间差。

在平时编写代码时我们并不是一定是需要使用这些关键字来保证程序的有序性,并且在程序运行时重排策略对我们是无感知的,这是因为我们在编写代码时,需要遵循happens-before原则来帮助我们辅助线程的原子性、可见性、有序性。

· happens-before原则

(1)程序次序原则:在一个线程内必须保证程序语义的串行性,按照代码顺序执行。

(2)管理锁定原则:对同一个锁需要先解锁再加锁,即加锁动作必须在解锁之后。

(3)volatile原则:对volatile变量的写操作,先发生于后面对这个变量的读操作。

(4)线程启动规则:Thread对象的start()方法执行先行发生于该Thread线程对象的所有运行动作之前。

(5)线程终止规则:线程中所有操作都先行发生于线程的终止检测。即通过Thread.join()、Thread.isAlive()方法检测线程是否终止前必须保证线程操作都已结束。

(6)线程中断规则:对线程interrupt()中断方法的调用先行发生于中断检测事件之前,可以通过Thread.interrupted()方法检测线程是否中断。

(7)对象终结原则:一个对象的初始化完成(构造函数执行结束)先行发生于他的finalize()方法之前。

(8)传递性,如果操作A先行发生于操作B,操作B先行发生于操作C,那么操作A必然先行发发生于操作C。

四、总结

JMM模型中允许主内存和工作线程中存在运行性能与工作线程相似的高速缓存。主内存中的数据是所有线程共享的,而线程中进行运算时需要将主内存中的数据复制到缓存中运算结束后再将运行结果同步到主内存中。在高并发的环境下,这样的工作过程和重排策略会导致线程安全问题。

在JMM中提供了Java关键字来保证线程的原子性、可见性、有序性。对于方法级别或者代码块级别的原子性问题,可以通过synchronized关键字来保证线程执行的原子性。在工作内存和主内存中同步数据存在延迟导致的可见性问题可以通过synchronized和volatile关键字来保证线程执行的可见性。对于指令重排导致的有序性问题,可以通过synchronized和volatile关键字来保证线程执行的有序性问题。

在编写代码时,我们也可以使用JMM内部定义的happens-before原则来辅助我们保证线程执行的原子性、可见性、有序性。

本文分享自微信公众号 - 码云大作战(gh_9b06dbcb85f3),作者:Y

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-05-30

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • NIO学习(二)Channel通道与Selectors选择器

    引用上一篇文章的区别。IO是传统的面向流的阻塞IO,而NIO是面向缓冲区的非阻塞式IO。在NIO中使用了一个线程来作为Selectors-选择器,来管理...

    虞大大
  • Spring知识点(五)代理模式

    使用代理模式的目的是为了将原来类生成一个代理类,由代理类来执行原来类的一些增强方法,但是也不影响原来类中方法的执行。

    虞大大
  • 多线程应用 - 基于AQS的Condition

    Condition类其实已经在之前的阻塞队列中有过分析。除了使用Synchronized关键字来作为同步锁外,ReentrantLock也可以代替Syn...

    虞大大
  • 并发编程JMM系列之基础!

    Java程序员在进行多线程开发时,并不需要关心线程间是如何通信的,这些对程序员本来来说完全是透明的,但是内存可见性问题很容易让我们困惑,今天我们就讲讲Java内...

    Java后端技术
  • pdd面经

    用户态和内核态概念(这个不懂。先表示了操作系统不太懂,然后就瞎扯了,被说不知道就别硬说了//手动捂脸)

    牛客网
  • MyBatis之Hello world(Mybatis入门)

    MyBatis中文网,超详细的:http://www.mybatis.org/mybatis-3/zh/index.html MyBatis英文网:http:/...

    别先生
  • 再见了,接码平台:交互式语音验证码

    和传统意义上的验证码(CAPTCHA)专治“人机识别”有些不一样,有时我们需要确认用户是否正在持有某个特定的设备(当然也可以顺便做一下人机识别)。 此时,我们通...

    FB客服
  • 【图文详解系列】JVM 内存模型

    进程计数器PC,当前线程所执行的字节码行号指示器。每个线程都有自己计数器,是私有内存空间,该区域是整个内存中较小的一块。

    一个会写诗的程序员
  • 单细胞测序如何指导临床问题?看这篇paper就够了

    几个月前的NGS创新者大会在杭州碰到了联川生物的沈总,说非常希望可以跟我们单细胞天地合作共同推广单细胞技术,就有了这个系列.

    生信技能树jimmy
  • 性能优化-jstack的使用

    有些时候我们需要查看下jvm中的线程执行情况,比如,发现服务器的CPU的负载突然增高了、出现了死锁、死循环等,我们该如何分析呢?

    cwl_java

扫码关注云+社区

领取腾讯云代金券