java内存模型《深入理解java虚拟机》

前言

首先我们来捋一下java内存模型(JMM)的由来,java内存模型是Java虚拟机规范为了屏蔽掉各种硬件和操作系统访问内存差异而制定的一种模型。

那么为什么要定义内存模型?其实上面的定义已经给了答案,就是为了实现内存屏蔽。那么接着问,为什么要实现内存屏蔽?因为要实现并发。

为什么要实现并发?实现并发的措施是什么?实现并发所带来的问题是什么?

首先回答下后面两个问题,首先实现并发的措施是多线程,其次实现并发后带来的问题是数据一致性无法保证,就是我们常说的线程安全。上面两个问题很多java程序员都知道的,但是其实对于大多数程序员来说为什么要实现并发是很难回答的,这里给出我认为的两个点:

  1. cpu的运算能力太强,单线程无法发挥出cpu强大的运算能力,导致cpu的资源有很大的浪费,这里的cpu运算能力的强大是相比较网络通信、IO操作及数据库访问的
  2. 服务端应用要提高TPS支持并发是一个不错的选择

到此我们简单总结下:java内存模型(JMM)是JVM为了实现多线程而做的内存屏蔽的一个约束。

java内存模型要做什么?

java内存模型的目的是定义程序中变量的访问规则,这个变量不是特指我们代码里的变量,但是可以类比我们的成员变量,JVM在定义变量访问规则的时候是围绕着在并发过程中如何处理原子性可见性有序性这三个特征来建立的。下面我们来看下这三个特征:

原子性(Atomicity):由Java内存模型来直接保证的原子性变量包括read、load、assign、use、store和write,我们大致可以认为基本数据类型的访问读写是具备原子性的。如果应用场景需要一个更大方位的原子性保证,Java内存模型还提供了lock和unlock操作来满足这种需求,尽管虚拟机未把lock和unlock操作直接开放给用户使用,但是却提供了更高层次的字节码指令monitorenter和monitorexit来隐式的使用这两个操作,这两个字节码指令反应到Java代码中就是同步块–synchronized关键字,因此在synchronized块之间的操作也具备原子性。

可见性(Visibility):可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。上文在讲解volatile变量的时候我们已详细讨论过这一点。Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的,无论是普通变量还是volatile变量都是如此,普通变量与volatile变量的区别是,volatile的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。因此,可以说volatile保证了多线程操作时变量的可见性,而普通变量则不能保证这一点。除了volatile之外,Java还有两个关键字能实现可见性,即synchronized和final.同步快的可见性是由“对一个变量执行unlock操作前,必须先把此变量同步回主内存”这条规则获得的,而final关键字的可见性是指:被final修饰的字段在构造器中一旦初始化完成,并且构造器没有把"this"的引用传递出去,那么在其他线程中就能看见final字段的值。

有序性(Ordering):Java内存模型的有序性在前面讲解volatile时也详细的讨论过了,Java程序中天然的有序性可以总结为一句话:如果在本线程内观察,所有的操作都是有序的:如果在一个线程中观察另外一个线程,所有的线程操作都是无序的。前半句是指“线程内表现为串行的语义”,后半句是指“指令重排序”现象和“工作内存与主内存同步延迟”现象。Java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性,volatile关键字本身就包含了禁止指令重排序的语义,而synchronized则是由“一个变量在同一个时刻只允许一条线程对其进行lock操作”这条规则获得的,这条规则决定了持有同一个锁的两个同步块只能串行的进入。

java内存模型做了什么?

java内存模型将内存划分为了主内存工作内存,以及定义了主内存和工作内存的交互规范。

主内存

java虚拟机规定所有的变量(不是程序中的变量)都必须在主内存中产生,为了方便理解,可以认为是堆区。

工作内存

java虚拟机中每个线程都有自己的工作内存,该内存是线程私有的为了方便理解,可以认为是虚拟机栈。虚拟机规定,线程对主内存变量的修改必须在线程的工作内存中进行,不能直接读写主内存中的变量。不同的线程之间也不能相互访问对方的工作内存。如果线程之间需要传递变量的值,必须通过主内存来作为中介进行传递。

简单总结下主内存和工作内存:主内存的数据是线程共享的,变量必须在主内存产生,线程只能操作自己的工作内存,不同线程的工作内存不能相互通信。下面看下主内存和工作内存是如何交互的

主内存和工作内存的交互

物理机高速缓存和主内存之间的交互有协议,同样的,java内存中线程的工作内存和主内存的交互是由java虚拟机定义了如下的8种操作来完成的,每种操作必须是原子性的(double和long类型在某些平台有例外,参考volatile详解和非原子性协定)
java虚拟机中主内存和工作内存交互,就是一个变量如何从主内存传输到工作内存中,如何把修改后的变量从工作内存同步回主内存。

  • lock(锁定):作用于主内存的变量,一个变量在同一时间只能一个线程锁定,该操作表示这条线成独占这个变量
  • unlock(解锁):作用于主内存的变量,表示这个变量的状态由处于锁定状态被释放,这样其他线程才能对该变量进行锁定
  • read(读取):作用于主内存变量,表示把一个主内存变量的值传输到线程的工作内存,以便随后的load操作使用
  • load(载入):作用于线程的工作内存的变量,表示把read操作从主内存中读取的变量的值放到工作内存的变量副本中(副本是相对于主内存的变量而言的)
  • use(使用):作用于线程的工作内存中的变量,表示把工作内存中的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时就会执行该操作
  • assign(赋值):作用于线程的工作内存的变量,表示把执行引擎返回的结果赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的字节码指令时就会执行该操作
  • store(存储):作用于线程的工作内存中的变量,把工作内存中的一个变量的值传递给主内存,以便随后的write操作使用
  • write(写入):作用于主内存的变量,把store操作从工作内存中得到的变量的值放入主内存的变量中

如果要把一个变量从主内存传输到工作内存,那就要顺序的执行read和load操作,如果要把一个变量从工作内存回写到主内存,就要顺序的执行store和write操作。对于普通变量,虚拟机只是要求顺序的执行,并没有要求连续的执行,所以如下也是正确的。对于两个线程,分别从主内存中读取变量a和b的值,并不一样要read a; load a; read b; load b; 也会出现如下执行顺序:read a; read b; load b; load a; (对于volatile修饰的变量会有一些其他规则,后边会详细列出),对于这8中操作,虚拟机也规定了一系列规则,在执行这8中操作的时候必须遵循如下的规则:

  • 不允许read和load、store和write操作之一单独出现,也就是不允许从主内存读取了变量的值但是工作内存不接收的情况,或者不允许从工作内存将变量的值回写到主内存但是主内存不接收的情况
  • 不允许一个线程丢弃最近的assign操作,也就是不允许线程在自己的工作线程中修改了变量的值却不同步/回写到主内存
  • 不允许一个线程回写没有修改的变量到主内存,也就是如果线程工作内存中变量没有发生过任何assign操作,是不允许将该变量的值回写到主内存
  • 变量只能在主内存中产生,不允许在工作内存中直接使用一个未被初始化的变量,也就是没有执行load或者assign操作。也就是说在执行use、store之前必须对相同的变量执行了load、assign操作
  • 一个变量在同一时刻只能被一个线程对其进行lock操作,也就是说一个线程一旦对一个变量加锁后,在该线程没有释放掉锁之前,其他线程是不能对其加锁的,但是同一个线程对一个变量加锁后,可以继续加锁,同时在释放锁的时候释放锁次数必须和加锁次数相同。
  • 对变量执行lock操作,就会清空工作空间该变量的值,执行引擎使用这个变量之前,需要重新load或者assign操作初始化变量的值
  • 不允许对没有lock的变量执行unlock操作,如果一个变量没有被lock操作,那也不能对其执行unlock操作,当然一个线程也不能对被其他线程lock的变量执行unlock操作
  • 对一个变量执行unlock之前,必须先把变量同步回主内存中,也就是执行store和write操作
    当然,最重要的还是如开始所说,这8个动作必须是原子的,不可分割的。
    针对volatile修饰的变量,会有一些特殊规定。
volatile修饰的变量的特殊性
  1. volatile修饰的变量对所有线程可见,换句话说volatile修饰的变量的值一旦改变会立马刷新到主内存,而且其他线程的工作内存在使用volatile变量时也必须重新加载主内存的新值,总结volatile是最轻量级的同步实现,但是volatile无法保证线程安全,原因是java代码不是原子性的,比如一行代码可能会被编译成多行字节码,同样一行字节码也可能被JIT编译成多行机器码,所以volatile无法实现线程同步,经典案例i++;
  2. volatile禁止指令重排序
同一个线程内观察所有指令都是有序的,一个线程内观察另一个线程的所有操作指令都是无序的

这句话怎么理解呢,就是在并发的情况下我们所写的代码都是无序,那么我们怎么保证我们程序的执行顺序,用volatile,还有synchronized的,但是这样的话我们的程序写起来就会很复杂,对于这种情况jvm提出了happens-before,happens-before翻译成中文是先行发生。

先行发生原则

先行发生原则是Java内存模型中定义的两个操作之间的偏序关系。比如说操作A先行发生于操作B,那么在B操作发生之前,A操作产生的“影响”都会被操作B感知到。这里的影响是指修改了内存中的共享变量、发送了消息、调用了方法等。个人觉得更直白一些就是有可能对操作B的结果有影响的都会被B感知到,对B操作的结果没有影响的是否感知到没有太大关系。

Java内存模型自带先行发生原则有哪些
  • 程序次序原则
    在一个线程内部,按照代码的顺序,书写在前面的先行发生与后边的。或者更准确的说是在控制流顺序前面的先行发生与控制流后面的,而不是代码顺序,因为会有分支、跳转、循环等。
  • 管程锁定规则
    一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须注意的是对同一个锁,后面是指时间上的后面
  • volatile变量规则
    对一个volatile变量的写操作先行发生与后面对这个变量的读操作,这里的后面是指时间上的先后顺序
  • 线程启动规则
    Thread对象的start()方法先行发生与该线程的每个动作。当然如果你错误的使用了线程,创建线程后没有执行start方法,而是执行run方法,那此句话是不成立的,但是如果这样其实也不是线程了
  • 线程终止规则
    线程中的所有操作都先行发生与对此线程的终止检测,可以通过Thread.join()和Thread.isAlive()的返回值等手段检测线程是否已经终止执行
  • 线程中断规则
    对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。
  • 对象终结规则
    一个对象的初始化完成先行发生于他的finalize方法的执行,也就是初始化方法先行发生于finalize方法
  • 传递性
    如果操作A先行发生于操作B,操作B先行发生于操作C,那么操作A先行发生于操作C。

简单总结下先行发生原则:先行发生与时间上的先后无关,注重的是影响,也就是先发生的操作的结果对后发生的行为可见。

总结

最后做一个总结,本文的主题是java内存模型,首先讲了Java内存模型是为了并发设计的,然后介绍了java内存模型围绕的原子性、可见性及有序性三大原则,接着介绍了主内存和工作内存及交互方式,紧接着介绍了volatile关键字,最后介绍的是先行发生原则。

扫码关注公众号

在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值