Java并发编程系列-volatile


原创文章,转载请标注出处:《Java并发编程系列-volatile》


一、概述

据说,volatile是java语言中最轻量级的并发控制方式。

volatile可以实现可见性、有序性,但是无法实现原子性,相对来说:synchronized可以实现这三个并发特性,所有我们可以使用synchronized来代替volatile,但是一直以来synchronized都已重量级闻名,其实在jdk1.5之后的版本中,java对synchronized进行了针对性优化处理,其操作速度已经不再是制约其是否可选择的因素,现在一般通过实际的情况来决定使用哪种方式。

二、特性

2.1 可见性

volatile可以保证变量的可见性,指的是什么呢?

可见性指的是在某一线程中对变量进行修改之后,其他线程可以立即发现并使用这个修改(一个线程的修改对其他线程可见)。

可见性的实现方式:volatile通过对java内存模型中主内存和工作内存交互方式的控制来实现。volatile确保一个线程对其修饰的变量的更改立即写入到主内存,同时确保每一次针对其修饰变量的读取操作直接从主内存中获取(即volatile强制将assign赋值操作和store、write操作绑定在一起,将use使用操作强制和read、load操作绑定在一起,这样assign之后必须执行store、write操作,use操作之前必须先执行read、load操作)这样就确保了其他线程读取到的变量的值是最新的(不熟悉这几个操作的同学请先了解java内存模型)。

volatile更底层的实现方式:

一个线程对volatile变量进行了修改之后,会写到工作内存,这一步映射到底层就是cpu将计算结果保存到高速缓存中,这时会触发一个LOCK指令,这个指令有两个作用,第一,它会锁定总线或者缓存,将修改后的新值保存到系统内存中,映射到JVM就是保存到主内存中。第二,它会将其他CPU的高速缓存中的这个变量的值置为无效,映射到JVM就是将其他线程的工作内存中保存的这个变量值置为无效,这样在这些其他线程要对变量进行操作时,读取变量时发现工作内存中的值是无效的,随即从主内存重新读取,并保存到工作内存。

这里还要说明一点,无论是主内存还是工作内存(系统内存还是高速缓存)都只是保存数据的部件或位置,所有针对变量的操作全部需要在CPU中进行,所以即使将数据从主内存(系统内存)读取到工作内存(高速缓存)中之后,想要操作,还需要从工作内存(高速缓存)中读取到CPU中的寄存器中进行计算。

2.2 非原子性

注意:volatile可以实现可见性,但是无法实现原子性。单个volatile变量的读写操作具有原子性,但是符合操作是与java代码相关的,并不是volatile这么一个关键字既可以控制得了的。(请将可见性和原子操作区分开来,我之前就混淆在一起,分开之后立即通透了)

java中实现原子操作的方式还是有很多的,但是并不包含volatile,简单的实现方式有:atomic包下的原子操作(通过CAS实现),基本数据类型的读写操作,加锁(synchronized或者Lock)实现等。

java中经典的非原子操作如自增实现,普通的i++操作看似只有一句话,但是编译成机器指令之后拥有多少行,不可知,肯定不是一句就能实现的,这么多命令要执行当然无法保证原子性,这时候我们可以用AtomicInteger和AtomicLong原子操作类的getAndIncrement()方法来实现,当然是用synchronized加锁同样可以实现。

2.3 有序性

volatile的另一个作用就是避免重排序优化,使用内存屏障的方式来实现禁止重排序优化。在单线程环境中当然没有必要禁止重排序优化,但是在多线程环境中指令重排序后执行就可能会出错,比如线程A中需要检测线程B中的某一个变量的值,依据这个值来进行某些操作。如果没有使用volatile修饰该变量,线程B中针对这个变量的操作就可能会发生重排序,可能会提前执行,这时一旦操作提前执行,那么线程A就可以会提前得到这个变量的值(或许是在一些线程A的准备工作还未全部准备好的情况,假设这些准备工作在线程B中定义,但是与变量操作无依赖关系,一旦变量操作提前,这些准备工作就会滞后,这时线程A就会在准备工作尚未完成的情况下启动执行后行代码,导致出错)。为变量加上volatile修饰之后,就会禁止其操作的指令重排序优化,保证所以的准备工作全部执行完成之后再进行变量操作,然后线程A在准备齐备的情况下启动,得以正常执行。

volatile有序性的实现原理是什么呢?

volatile底层通过内存屏障的方式来禁止重排序优化,具体来说,JMM采用的是保守策略,所谓保守策略,就是通过冗余的内存屏障来保证所有影响Volatile功能的重排序全部被禁止,保证volatile功能的完整性,此处冗余的意思就是,可能会存在多余的内存屏障,但这种多余的内存屏障是不影响操作执行的,或者说是有的内存屏障所禁止的重排序操作如果实际发生了重排序也不会影响操作结果的情况,但是这种冗余的内存屏障可以排除那种任何特殊情况来确保volatile功能的完整性。

内存屏障包括:

  1. volatile变量写操作之前的storestore屏障,这个屏障保证所有在volatile写操作之前的任何操作都不能被重排序到volatile写操作之后,确保volatile变量写操作的正确性,因为所有直接或间接的修改都在写操作之前完成了,那么写的变量值一定是最终的正确值。
  2. volatile变量写操作之后的storeload屏障,这个屏障保证所有在volatile写操作之后的任何操作都不能被重排序到volatile写操作之前,确保volatile变量写操作的正确性,其实这里真正禁止的是其后可能出现的volatile读写操作被重排序到这个写操作之前。
  3. volatile变量读操作之后的loadstore屏障,这个屏障保证所有在volatile读操作之后的任何读操作都不会被重排序到volatile读操作之前,确保volatile变量读操作的正确性。
  4. volatile变量读操作之后的loadstore屏障,这个屏障保证所有在volatile读操作之后的任何写操作都不会被重排序到volatile读操作之前,确保volatile变量读操作的正确性。

上面的内容并不好理解和记忆,我们可以总结如下:

  • 所有Volatile写操作之前的操作禁止重排序到该写操作之后
  • 所有volatile读操作之后的操作禁止重排序到该读操作之前
  • volatile写操作之后是volatile读操作时,禁止重排序

记住上述三点就可以了!

三、使用

在涉及到并发操作的情况下,可以优先考虑是否可以使用volatile来解决问题,适合的场景如下:

在只涉及可见性,针对变量的操作只是简单的读写(保证操作的原子性)的情况下可以使用volatile来解决高并发问题,如果这时针对变量的操作是非原子的操作,这时如果只是简单的i++式的操作,可以使用原子类atomic类来保证操作的原子性(采用CAS实现),如果是复杂的业务操作,那么舍弃volatile,采用锁来解决并发问题(synchronized或者Lock)。

四、总结

volatile可以实现可见性和有序性,无法实现原子性 ,简单的原子操作可以委托给其他方式进行实现,复杂的原子操作需要借助锁来实现,这时候完全没有必要加上volatile了,因为锁已经包含了volatile的功能。

猜你喜欢

转载自blog.csdn.net/weixin_34406796/article/details/87550370