剖析Java内存模型

本文详细解析了Java内存模型的核心概念,包括主内存与工作内存的交互、volatile变量的特殊规则,以及原子性、可见性、有序性原理。重点讲解了volatile变量如何保证可见性和禁止指令重排序,以及先行发生原则在并发编程中的应用。
摘要由CSDN通过智能技术生成

2.Java内存模型

1.主内存与工作内存

  1. Java内存模型的主要目的是关注在虚拟机中把变量值存储到内存和从内存中取出变量值。PS:此处的变量包括了实例字段、静态字段、构成数组对象的元素,但是不包括局部变量和方法参数,因为后者是线程私有的,不会被共享,自然不存在竞争问题。
  2. Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,保存了被该线程使用的变量的主内存副本,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的数据。
    不同的线程之间也无法直接访问对方工作内存中的变量,传递需要通过主内存来完成。
    从更基础的层次上说,主内存直接对应于物理硬件的内存,而为了获得更好的运行速度,虚拟机会让工作内存优先存储于寄存器和高速缓存中,因为程序运行时主要访问的是工作内存。
    PS:如果局部变量是一个reference类型,它引用的对象在Java堆中可被各个线程共享,但是本身在Java栈的局部变量表中是线程私有的。

2.内存间交互操作

  1. 关于主内存与工作内存之间具体的交互协议,Java内存模型定义了以下8种操作来完成。虚拟机实现时必须保证下面的操作都是原子的、不可再分的。
    lock(锁定):作用于主内存的变量,它把一个遍历标识为一条线程独占的状态
    unlock(解锁):作用于主内存的变量,它把一个锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
    read(读取):作用于主内存的变量,它把变量的值从主内存传输到线程的工作内存中,以便随后load动作使用
    load(载入):作用于工作内存的变量,它把read操作得到的变量值放入工作内存的变量副本中。
    use(使用):作用于工作内存,把一个遍历的值传递给执行引擎,每当虚拟机需要使用变量的值的字节码指令会执行这个操作
    assign(赋值):作用于工作内存,把一个从执行引擎接收的值赋给工作内存的变量,遇到给变量赋值的字节码指令时会执行
    store(存储):作用于工作内存,把工作内存的变量的值传送到主内存中,以便随后的write操作使用。
    write(写入):作用于主内存,把store操作从工作内存得到的变量的值放入主内存的变量中。
  2. 如果把变量从主内存拷贝到工作内存,那就要按顺序执行read和load操作;
    如果把变量从工作内存同步回主存,就要按顺序执行store和write操作。
    Java内存模型只要求上诉2个操作必须按顺序执行,但不要求是连续执行。所以它们之间可插入其他指令。
    如read a、read b、load b、load a。
  3. 在执行上诉8种基本操作时必须满足如下规则:
    1.不允许read和load,store和write操作之一单独出现。
    2.不允许一个线程丢弃它最近的assign操作,即变量在工作内存改变了必须把该变化同步回主内存
    3.不允许一个线程无原因地(没发生过任何assign操作)把数据从工作内存同步回主内存中。
    4.一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化的变量。
    (在对一个变量实施use,store操作之前,必须先执行assign和load操作)
    5.一个变量在同一个时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多 次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。
    6.如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新 执行load或assign操作以初始化变量的值。
    7.如果一个变量事先没有被lock操作锁定,那就不允许对它进行unlock操作,也不允许去unlock一个被其他线 程锁定的变量。
    8.对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)
  4. Java内存模型的8种操作也可简化为read、write、lock、unlock四种,但这只是语言描述上的等价化简,Java内
    存模型的基础设计并未改变。

3.对于volatile型变量的特殊规则

  1. 当一个变量被定义为volatile之后,它具备2项特性:
    1.可见性:保证此变量对所有线程的可见性,即一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。而普通变量的值在线程间传递时均需要通过主内存来完成。
    volatile会在本处理器的缓存写入内存时,引起别的处理器无效化其缓存。
    但是基于volatile变量的运算在并发下不一定是线程安全的,可以认为不存在一致性的问题,但是Java里面的运算操作符并非原子操作,这导致volatile变量的运算在并发下一样是不安全的。
    2.禁止指令重排序优化,普通的变量只会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作与程序代码中的执行顺序一致。因为在同一个线程的方法执行过程中无法感知到这点。volatile会在写操作时插入许多内存屏障指令来保证处理器不发生乱序执行。
  2. 即使编译出来只有一条字节码指令,也并不意味着执行这条指令就是一个原子操作。一条字节码指令也可能转化成若干条本地机器码指令。使用-XX:+PrintAssembly参数输出反汇编来分析会更严谨。
  3. 由于volatile变量只能保证可见性,以下场景可以不需要加锁。
    1.运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
    2.变量不需要与其他的状态变量共同参与不变约束。
  4. Java内存模型中对volatile变量定义的特殊规则的定义。假设T表示一个线程,V和W表示volatile变量。
    1.只有当线程T对变量V执行的前一个动作是load的时候,线程T才能对变量V动作use动作;相反也是一样。
    load和read动作是相关联的,必须连续且一起出现。
    这条规则要求在工作内存汇总,每次使用V前都必须先从主内存刷新最新的值,保证能看见其他线程的修改。
    2.store和write动作也是相关联的,必须连续且一起出现。
    这条规则要求在工作内存中,每次修改V后都必须立刻同步回主内存,用于保证其他线程可以看到修改。
    3.volatile修饰的变量不会被指令重排序优化,从而保证代码的执行顺序与程序的顺序相同。

4.针对long和double型变量的特殊规则

  1. 对于64位的数据类型,允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为2次32位的操作来进行。即允许虚拟机实现自行悬着是否要保证64位数据类型的load、store、read、write这4个操作的原子性。这就是所谓的"long和double的非原子性协定"。
  2. 如果有多个线程共享一个并未声明为volatile的long或double类型的变量,并且同时对它们进行读取和修改操作,那么某些线程可能会读取到一个既不是原值,也不是其他线程修改值的代表半个变量的数值。但是这种情况是非常罕见的。

5.原子性,可见性,有序性

  1. 原子性:基本数据类型的访问。读写都是具备原子性的。
    如果应用场景需要一个更大范围的原子性保证,Java内存模型海提供了lock和unlock操作。尽管这2个操 作没有直接开放给用户使用,但是却提供了更高层次的字节码指令monitorenter和monitorexit来隐式地 使用这2个操作。这2个字节码反映到Java代码就是同步块——synchronized关键字。
  2. 可见性:当一个线程修改了共享变量的值时,其他线程能够立即得知这个修改。
    除了volatile之外,Java还有2个关键字能实现可见性,它们是synchronized和final。
    同步块的可见性是由对一个变量执行unlock操作之前,必须先把此变量同步回主内存中这条规则获得的。
    lock之前必须清空工作内存中共享变量的值。
    final关键字的可见性是指:被final修饰的字段在构造器中一旦被初始化完成,并且构造器没有把this的引用传递出去(this引用逃逸是一件很危险的事,其他线程有可能通过这个引用访问到初始了一半的对象),那么再其他线程中就能看到final字段的值。
  3. 有序性:如果在本线程内观察,所有的操作都是有序的;如果在一个线程观察另一个线程,所有操作都是无序的。前半句指线程内似表现为串行的语义。后半句是指指令重排序现象和工作内存与主内存同步延迟现象。
    Java提供了volatile和synchronized2个关键字来保证线程之间操作的有序性,volatile本身就包含了禁止指令重排序的语义,而synchronized则是由一个变量在同一个时刻只允许一条线程对其进行lock操作这条规则获得的。

6.先行发生原则

  1. 如果Java内存模型中所有的有序性都仅靠volatile和synchronized来完成,那么有很多操作都将会变得非常啰嗦,但是编写Java并发代码的时候并没有察觉到这一点,这是因为有一个先行发生(happens-before)原则。
    这个原则非常重要,它是判断数据是否存在竞争,线程是否安全的非常有用的手段。
  2. 下面是Java内存模型下一些天然的先行发生关系,如果2个操作之前的关系不在此列,并且无法从下列规则推导出来,则它们就没有顺序性保障,虚拟机可以 对它们随意地进行重排序。
    1.程序次序规则:在一个线程内,按照控制流顺序,书写在前面的操作先行发生于书写在后面的操作
    PS:这里说的控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构。
    2.管程锁定规则:一个unlock操作先行发生于后面对同一个操作的lock操作。这个强调的是"同一个锁"的lock操作
    3.volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作。
    4.线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作。
    5.线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测。
    6.线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
    7.对象终结规则:一个对象的初始化完成先行发生于它的finalize()方法的开始。
    8.传递性:如果操作A先行发生于操作B,操作B先行发生于操作C,则操作A先行发生于操作C。
  3. 一个操作时间上的先发生不代表这个操作会是先行发生。时间先后顺序与先行发生原则之间基本没有因果关系,所以我们衡量并发安全问题的时候不要受时间顺序的干扰,一切必须以先行发生原则为准。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值