【Java】Java内存模型

1.什么是Java内存模型

Java内存模型(Java Memory Model,JMM)是一种抽象的,不存在的概念。是一种屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。
Java内存模型具体定义了:所有的变量(包括类变量、成员变量,但是不包括方法内局部变量)都应该存储在主内存中;每个线程都有自己的工作内存空间,线程对变量(为主内存变量副本)的读取、赋值操作必须在自己的工作内存进行,不能直接对主内存中的变量值进行操作;不同线程之间无法访问对方工作内存中的变量,线程之间的变量值传递需要经过主内存。主内存、线程、以及工作内存三者之间的协同关系,如下图所示:
在这里插入图片描述
JMM 描述的是一组规则,通过这组规则控制各个变量在共享数据区域内和私有数据区域的访问方式,JMM是围绕原子性、有序性、可见性展开。与Java内存区域模型的Java堆、栈、方法区本质上不是同一层次的内存划分规则,两种维度之间没有直接对应关系。 如果非要将Java内存模型中的工作内存和主内存一定要落实到对应的Java内存区域模型中时,工作内存可以对应为栈,而主内存则可以对应为堆。

2.主内存、工作内存

主内存主要存储的是Java实例对象,所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量还是方法中的本地变量(也称局部变量),当然也包括了共享的类信息、常量、静态变量。在主内存中的实例对象可以被多线程共享,多个线程同时调用类同一个对象的同一个方法,那么两个线程会将要操作的数据拷贝一份到直接的工作内存中,执行完操作后才刷新到主内存。由于是共享数据区域,多个线程访问同一个变量可能会引发线程安全问题。
工作内存主要存储当前方法的所有本地变量信息,是主内存中的变量副本拷贝,每个线程只能访问自己的工作内存,即线程中的本地变量对其他线程是不可见的,就算是两个线程执行的是同一段代码,它们也会在各自的工作内存中创建属于当前线程的本地变量,由于工作内存是每个线程的私有数据,线程间无法相互访问工作内存,因此存储在工作内存的数据不存在线程安全问题。需要注意的是,如果本地变量是基本数据类型,不会受到Java内存模型的访问方式限制,因为这种变量是直接存储在自己的线程栈帧的。

3.Java内存模型遵循的原则

3.1.原子性

原子性指的是一个操作不可中断,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响。
Java内存模型中定义了主内存与工作内存之间的交互方式,明确了一个变量如何从主内存拷贝到工作内存,以及如何从工作内存同步到主内存空间。这些操作细节主要由8种操作来实现,每一种实现都要求是原子性操作,主要如下所示:

  • lock(锁定):作用于主内存变量,把一个变量标识为一个线程独占状态
  • unlock(解锁):作用于主内存变量,把处于锁定状态的变量释放,释放后才可以被其他线程操作
  • read(读取):作用于主内存变量,把变量值从主从读取到工作内存,为load动作做准备
  • load(载入):作用于工作内存变量,把read操作读取到的变量值放入工作内存的变量副本中
  • use(使用):作用于工作内存变量,把该变量值传递给执行引擎(每当虚拟机指令需要使用变量时触发)
  • assign(赋值):作用于工作内存变量,把从执行引擎接收到的值赋值给该变量(每当虚拟机赋值触发动作)
  • store(存储):作用于工作内存变量,把工作内存的变量值传递给主内存,为write操作做准备
  • write(写入):作用于主内存变量,把store操作从工作内存传递过来的值放入主内存变量中

Java内存模型还定义了这些操作之间的组合性、顺序性问题。如:把一个变量从主内存拷贝到工作内存需要经过read和load操作,从工作内存拷贝到主内存需要经过store和write操作,其中read和load,store和write操作只要求顺序,不要求连续,他们执行的过程中间可以允许插入其他指令,例如:read a、read b、load b、load a。Java内存模型还规定了操作时必须满足的规则:

  • 不允许read、load、store、write操作单独出现,不允许线程丢弃最近的assign操作,在工作内存赋值后必须同步写回主内存
  • 不允许线程无原因的把数据从工作内存同步到主内存(未发生过assign操作)
  • 变量只能在主内存中诞生,不允许工作内存直接使用未初始化的变量,use、store之前必须先load、assign
  • 变量在同一时刻只允许一个线程进行lock,lock可以多次,但是unlock需要等同lock的次数
  • 变量执行lock操作,将清空工作内存该变量的值,再次进行load或assign初始化变量的值
  • 未进行lock操作的变量不允许对其进行unlock操作,也不允许unlock被其他线程锁定的变量
  • 变量进行unlock操作前必须先进行store和write操作

Java内存模型中的八种基本操作以及顺序约束,保证的是并发操作下的安全性。多线程编程时保持原子性,可以使用Java的java.util.concurrent包,它将操作简化为read、write、lock、unlock等相关API工具类;可以利用 JVM 对基本数据类型读写操作的原子性;还可以通过 synchronized 实现原子性。
另外,有一点需要说明,对于32位系统来说,long 类型数据和 double 类型数据的读写并非原子性的,也就是说如果存在两条线程同时对 long 类型或者 double 类型的数据进行读写是存在相互干扰的,因为对于32位虚拟机来说,每次原子读写是32位,而 long 和 double 则是64位的存储单元,这样导致一个线程在写时,操作完成前32位的原子操作后,轮到B线程读取时,恰好只读取来后32位的数据,这样可能回读取到一个即非原值又不是线程修改值的变量,它可能是“半个变量”的数值,即64位数据被两个线程分成了两次读取。

3.2.有序性

在执行程序时,为了提高性能,编译器和处理器常常会对指令进行重排序。保证有序性就是保证重排序后指令的执行顺序与期望一致。
Java内存模型并非在任何情况下都会进行重排序。下面几种常见情况就不会改变执行顺序:

  • Java编译器在生成指令序列的时候会禁止特定类型的处理器&编译器进行重排序的处理,并且重排序优化需遵循数据的依赖性。所谓依赖性是指如果两个操作同时访问一个变量,构成数据上下文依赖。
  • 满足先行发生原则(happens-before原则)。JVM中已经内置了一些先行发生原则:
    • 程序次序规则(Program Order Rule):在一个线程内,按照控制流顺序,书写在前面的操作先行发生于书写在后面的操作。注意,这里说的是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构。
    • 管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调的是“同一个锁”,而“后面”是指时间上的先后。
    • volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面对这个变量的读操作,这里的“后面”同样是指时间上的先后。
    • 线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作。
    • 线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread::join()方法是否结束、Thread::isAlive()的返回值等手段检测线程是否已经终止执行。
    • 线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread::interrupted()方法检测到是否有中断发生。
    • 对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。
    • 传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。

处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应的机器指令的执行顺序;指令重排只会保证单线程中串行语义的执行的一致性,但并不会关心多线程间的语义一致性。这时重排后将会有可能出现指令顺序错误的问题。
多线程编程时保持有序性,可以通过Java的 volatile 关键字来保证一定的“有序性”;另外可以通过 synchronized 和 final。

3.3.可见性

可见性指的是当一个线程修改了某个共享变量的值,其他线程是否能够马上得知这个修改的值。在多线程环境,由于线程对共享变量的操作都是线程拷贝到各自的工作内存进行操作后才写回到主内存中的,这就可能存在一个线程A修改了共享变量的值,还未写回主内存时,另外一个线程B又对主内存中同一个共享变量进行操作,但此时A线程工作内存中共享变量对线程B来说并不可见,这种工作内存与主内存同步延迟现象就会造成可见性问题,另外指令重排以及编译器优化也可能导致可见性问题。
Java的 volatile 关键字可以保证可见性。当一个共享变量被 volatile 关键字修饰时,它会保证修改的值立即被其他的线程看到,即修改的值立即更新到主存中,当其他线程需要读取时,它会去内存中读取新值。synchronized 也可以保证可见性。

4.参考

https://www.cnblogs.com/starsray/p/16395937.html
https://segmentfault.com/a/1190000037799975
https://www.cnblogs.com/zh94/p/14109703.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值