java内存模型

java内存模型

定义java内存模型可以屏蔽各种硬件和操作系统的内存访问差异,实现让java程序在各个平台能够达成一致的内存访问效果。
java内存模型的目的是:定义程序中各个变量的访问规则(即把变量值存储到内存以及从内存中取出这样的底层细节)。注这里的变量为实例变量、静态变量还有构成数组对象的元素。不包括方法中定义的变量,以为方法最终转化为栈帧,放到虚拟机栈中,是线程私有的,不存在共享。

主内存和工作内存

在这里插入图片描述

  • 主内存作用(可以视为硬件中的主存,主内存是虚拟机内存的一部分):
    所有变量都存储在主内存中,不同线程之间变量的传递必须通过主内存进行(线程无法直接访问别的线程的工作内存)
  • 工作内存作用(可以与高速缓存、寄存器类比):
    保存了被线程使用的变量在主内存的副本,线程对变量的所有操作,都必须把变量从主内存中读入工作内存中进行。这里的副本并不是把整个对象复制到工作内存中,而是把对象的引用、本次被访问到的对象中的某个字段复制过去。

内存间交互操作(变量从从主存拷贝到工作内存,再从工作内存同步到主存的过程)

java内存模型中定义了8种院子操作来完成工作内存与主存的交互:
(1)lock(锁定):作用于主内存变量,锁定变量,其他线程不能访问(读、写)。
(2)unlock(解锁):与lock相对。
(3)read(读取):作用于主内存变量,将一个变量值从主内存传输到线程工作内存,以便load动作使用。
(4)load(载入):作用于工作内存变量,把read操作读入的值放入工作内存的变量副本中。可以理解为把主存读出来的值放入操作数栈的局部变量表的变量槽中
(5)use(使用):作用于工作内存变量,把工作内存中一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时就会执行这个操作。可以理解为把变量从从变量槽放入操作数栈中,供指令使用
(6)assign(赋值):作用于工作内存变量,把一个从执行引擎接受的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。可以理解为计算完成后只是把变量值压入栈顶,然后通过assign把变量值放入局部变量表的变量槽中
(7)store(存储):作用与工作内存变量,把工作内存中一个变量值传送到主内存中,以便随后的write操作。
(8)wirte(写如):作用于主内存变量,把store操作懂工作内存中得到的变量值放入主内部才能的变量中。
在这里插入图片描述
注:
(1)把变量从主内存拷贝到工作内存,就要按顺序执行read和load操作;如果要把变量从工作内存同步回主内存,就要按顺序执行store和write操作。java内存模型只要求以上两个操作必须顺序执行,不要求连续执行,即read和load之间,store和write操作可以插入其他指令。如某线程访问主内存中的变量a,b,一种可能出现的顺序是read a,read b,read b, read a。
(2)还规定在执行以上八种基本操作,必须满足如下规则:
(a) read与load、store与wirte不能够单独出现,即不允许从主存中读取的变量值但是工作内存不接受或者从工作内存同步回主存但是主存不接受。
(b)如果工作内存中变量进行了assign(即变量发生了修改),那么必须把变量值同步回主存,并且只有做了修改的变量值才能写回主存。
(c)线程要使用一个变量,必须从主存中read–load到工作内存,不能在工作能存中产生变量。
(d)一个变量可以被同一个线程lock多次,并且要进行相同次数的unlock,该变量才能被其他线程使用。
(e)线程对变量进行lock操作,那么所有线程工作内存中的该变量值都会被清空(无效)。下次使用时,需要从新read -> load到工作内存。
(f)对一个变量进行unlock之前,必须把该变量同步回主内存(即执行store、wirte操作)

Volatile修饰符

见自己写的另外一篇

对long和double类型变量的访问(long和double的非原子性协定)

对于64位的数据类型,允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为两次32位的操作来进行。(因此一个long或者double类型的共享变量,一条线程在读取了一半该变量后(只读取了高32位)再去去读时可能低32位已经被其他线程修改了)
在64位虚拟机中不会出现这种非原子性访问行为,在32位虚拟机中对long类型的数据操作存在非原子性访问风险,从jdk9起,HotSpot增加了参数-XX:+AlwaysAtomicAccesses来让虚拟机对所有数据类型都进行原子操作。对于double,现在的cpu包含专门的浮点运算器,专门处理单精度和双精度浮点数,因此不会出现非原子性访问的问题。

原子性、可见性、有序性(并发三大特征)

  • 原子性
    (1)read、load、use、assign、store、write6个操作保证了基本数据类型的访问、读写具备原子性;long、double是个例外。
    (2)如果需要更大范围的原子性,java内存模型提供lock和unlock操作,虽然未直接开放给用户,但是提供了字节码指令monitorenter和monitorexit来隐式的使用lock和unlock。synchronized关键字编译为字节码文件时就转化为monitorenter和monitorexit,故synchronized修饰的代码段具备原子性,因此synchronized最底层应该是lock和unlock实现。
  • 可见性
    (1)可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。
    (2)无论是普通变量还是volatile变量,java内存模型都会把变量修改后将新值同步会主内存,在变量读取前从主内存刷新变量值到工作内存,java内存模型依赖主内存作为传递媒介实现线程间变量可见性。但是普通变量与volatile变量的区别是,volatile保证了变量在工作内存中被修改后立即同步(store、write)到主内存,以及在主内存中使用(use)该变量时需要立即从主内存中刷新(read、load)到工作内存,这样保证每次使用时是内存中的最新值。而普通变量做不到立即,即变量在工作内存中被修改后,可能会过一段时间再同步会主内存(同步延迟现象)。
    (3)synchronized和final关键字也能保证可见性。
    synchronized修饰的代码块的可见性是由“对一个变量执行unlock之前,必须把该变量同步回主内存中”这条规则获得。
    final关键字的可见性是指(final修饰的字段是不可变的,故所有线程看到的该值都是一样,但是如果构造器中this引用发生了逃逸(一般是构造器中启动了线程或者注册了监听器),导致其他线程能够通过this引用访问还没有初始化完成的对象(可能就包括final变量没有初始化化完毕),那么两个线程看到的final值不一样):被final修饰的字段在构造器中一旦被初始化完成,并且构造器没有把this引用传递出去(this引用逃逸可能使其他线程通过该引用访问到“初始化了一半”的对象),那么其他线程中就能看见final字段的值,因为一旦final变量被初始化完成就不可改变,故所有线程看见的final值都是一样的。
  • 有序性
    (1)java程序中天然的有序性:如果在本线程中观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。前半句指“线程类似表现为串行的语义”,后半句指“指令重排序”现象和“工作内存与主内存同步延迟”现象。
    (2)volatile和synchronized保证线程之间的有序性。volatile本身包含禁止指令重排序语义,synchronized则由“一个变量在同一个时刻只允许一条线程对其进行lock操作”这条规则获得,决定了持有同一个锁的两个同步块只能串行地进入。

先行发生原则(Happens-Before)–判断多线程环境下线程是否安全

  • 先行发生是java内存模型中定义的两项操作之间的偏序关系,比如说操作A先行发生于操作B,起始就是说在发生B之前,A操作产生的影响能够被B观察到。
  • 一下是java内存模型天然的先行发生关系,如果两个操作之间的关系不在此列,则这两个操作没有顺序性保障,虚拟机可以对他们随意重排序。也就是说如果不满足一下关系,那么多线程环境下就无法保证线程安全:
    • 程序次序规则(单线程):一个线程中,按照控制流顺序(注意不是代码顺序,因为要考虑分支、循环等),书写在前面的先行发生与书写在后面的操作。–在一个线程中,程序是完全按照控制流顺序执行了
    • 管程锁定规则(synchronized):一个unlock操作先行发生与后面对同一个锁的lock操作。–即对于同一个锁,如果已经被一个线程获得,那么其他线程只有等到该线程释放锁(unlock)时才能获得
    • volatile变量规则(voliate):对一个volatile变量的写操作先行发生于后面对这个变量的读操作。
    • 线程启动规则:Thread对象的start()方法先行发生于该线程的每一个动作。
    • 线程终止规则:线程所有操作都先行发生于对次线程的终止检测。可以通过Thread::join()方法是否结束(在A线程中调用B线程的join方法,那么A线程放弃CPU资源等待B线程执行完毕后,A线程才能执行,底层是用wait()/notifyAll()方法实现的,A线程在执行到B线程的join()方法时,需要获得B线程的锁,然后在join()方法中调用wait()方法,释放锁,等待B线程执行完毕后调用notifyAll()方法,让A线程继续获得锁,继续执行,这样保证的串行执行),也可以通过Thread::isAlive()返回值检测线程是否终止。
    • 线程中断规则:线程interrupt()方法调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread::interrupted()方法检测是否中断发生
    • 对象终结规则:对象的初始化完成先行发生于他的finalize()方法的开始
    • 传递性规则:A 操作先行发生于B,B操作先行发生于C,那么A先行发生于C
    • 在这里插入图片描述
      时间先后顺序(代码先后顺序)和先行发生原则之间基本没有因果关系
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值