Java并发编程之内存模型与volatile关键字

一、Java内存模型

Java内存模型是为了屏蔽不同硬件和操作系统间的内存访问差异,以实现Java在不同平台下都能有一致的内存访问效果。

Java内存模型主要定义了程序中各种变量的访问规则,既关注虚拟机中把变量值存储到内存和从内存中取出变量值这样的底层细节。(此处的变量只包括线程间共享的变量:实例字段、静态字段和构成数组对象的元素)

Java内存模型规定了所有的变量都存储在主内存(虚拟机内存的一部分)中。每条线程分别有自己的工作内存,工作内存中保存了被该线程使用的变量的主内存副本,线程对变量的所有操作(读取、修改等)都是对工作内存中的副本进行,而不能直接读写主内存中的数据。线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
注意:副本复制的是字段,并不会将整个实例复制一遍,那样太浪费空间。

线程、主内存、工作内存三者的交互关系如下图所示。
在这里插入图片描述

Java内存模型与JVM内存模型(Java运行内存)

Java 内存模型和JVM内存模型是不一样的东西。
JVM内存模型是指Jvm运行时将数据分区域存储,强调对内存空间的划分
而内存模型是定义了线程和主内存之间的抽象关系,即定义了JVM 在计算机内存(RAM)中的工作方式,如果我们要想深入了解Java并发编程,就要先理解好Java内存模型。

在这里插入图片描述

内存间交互操作

关于主内存与工作内存之间具体的交互协议,既一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存这一类的实现细节,Java内存模型中定义了8种操作来完成。

  • 作用于主内存变量的操作

    • lock(锁定):将一个变量标识为一条线程独占的状态。
    • unlock(解锁):将处于锁定状态的变量解放出来。
    • read(读取):将一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
    • write(写入):把store操作从工作内存中得到的变量的值放入主内存的变量中。
  • 作用于工作内存变量的操作

    • load(载入):把read操作从主内存中得到的变量值放入工作内存的变量副本中。
    • use(使用):把工作内存中一个变量的值传给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
    • assign(赋值):把一个从执行引擎接受的值赋给工作内存中的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
    • store(存储):把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。

如果要将一个变量从主内存中拷贝到工作内存中则要顺序执行read和load操作。
如果要将一个变量从工作内存中同步回主内存则要顺序执行store和write操作。

二、volatile关键字

volatile关键字是Java虚拟机提供的最轻量级的同步机制。
当一个变量被定义成volatile之后,它将具备两项特性:可见性和有序性

可见性

1、保证此变量对所有线程的可见性。
这里的可见性是指当一个线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。当写一个volatile变量时,JMM会把该线程对应的本地内存中的值同步到主内存。当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

而普通变量做不到这一点,普通变量的值在线程间传递时均要通过主内存来完成。例如:线程A修改了一个普通变量的值,然后向主内存进行回写,另一个线程B需要在A回写完成了之后再主动对主内存进行读取操作,新变量才会对线程B可见。也就是如果线程B一直不读,则数据一直不会更新。

关于volatile修饰的变量的可见性,经常会被误认为以下描述是正确的:
“volatile变量对所有线程是立即可见的,对volatile变量所有的写操作都能立刻反映到其他线程之中。换句话说,volatile变量在各个线程中是一致的,所以基于volatile的运算在并发下是线程安全的”。

这句话的论据部分并没有错,但是其并不能得出“基于volatile的运算在并发下是线程安全的”这个结论。
volatile变量在各个线程的工作内存中是不存在一致性问题的(每次修改都要刷新,不存在一致性问题),但是虽然这也只能保证读取时的一致性,Java中的运算操作符并非是原子操作,这会导致volatile变量的运算在并发下是线程不安全的。

例如,假设现在有多个线程同时对一个volatile变量a进行a++操作,那么多个线程在进行a++这个操作时,会先获取a的值,然后再进行+1的操作,在获取a的值的时候可能别的线程已经对a进行了+1导致值过期。

有序性

关于Java内存模型中的有序性可以总结为:如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。前半句是指“线程内表现为串行语义”,后半句是指“指令重排序”现象和“工作内存主主内存同步延迟”现象。

在Java内存模型中,为了效率是允许编译器和处理器对指令进行重排序,当然重排序不会影响单线程的运行结果,但是对多线程会有影响。Java提供volatile来保证一定的有序性。另外,可以通过synchronized和Lock来保证有序性,synchronized和Lock是保证每个时刻是有一个线程执行同步代码,相当是多个单线程执行代码,自然就保证了有序性。

volatile变量的第二个特性就是有序性。它会禁止指令重排序优化。

总结

在某些情况下,volatile的同步机制的性能确实要优于锁,但是由于虚拟机对锁实行的许多消除和优化,使得我们很难确切地说volatile就会比synchronized快上多少。如果让volatile自己与自己比较,那可以确定一个原则:volatile变量读操作的性能消耗与普通变量几乎没有什么差别,但是写操作则可能会慢上一些,因为它需要在本地代码中插入许多内存屏障来保证有序性。不过即便如此,大多数场景下volatile的总开销仍然要比锁来得更低。我们在volatile与锁中选择的唯一判断依据仅仅是volatile的语义能否满足使用场景的需求。

参考文献:

深入理解Java虚拟机第3版

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值