「volatile」是java中保证有序性、可见性的关键字,相比于synchronized来说他更轻量,是jvm提供的最轻量的同步机制。之前我们介绍的ReentrantLock可重入锁里的状态变量state,就是被volatile所修饰的,ConcurrentHashMap里的node节点里的value和next同样被其修饰。
在并发包里,通过volatile实现可见性、有序性,那么并发编程中还要求的一个原子性是怎么保证的呢?答案是CAS比较并交换,关于CAS的介绍我们之前也说过了。
什么是可见性?简单来说,就是多个线程共同访问某个共享变量时,某个线程修改了此变量,其他线程能立即看到修改后的值。
在说volatile之前,我们先说下synchronized是如何保证可见性的:
「JMM」(java内存模型)关于synchronized有两条规定:
回到volatile,为了提高处理器的执行速度,我们在处理器和内存之间增加了多级缓存来提升速度。但是由于引入了多级缓存,就存在缓存数据不一致问题。
但是,对于volatile变量,当对volatile变量进行写操作的时候,JVM会向处理器发送一条lock前缀的指令,将这个缓存中的变量回写到系统主存中。
但是就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题,所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现「缓存一致性协议」。
「缓存一致性协议」:每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里。
所以,如果一个变量被volatile所修饰的话,在每次数据变化之后,其值都会被强制刷入主存。而其他处理器的缓存由于遵守了缓存一致性协议,也会把这个变量的值从主存加载到自己的缓存中。这就保证了一个volatile在并发编程中,其值在多个缓存中是可见的。
volatile是通过编译器在生成字节码时,在指令序列中添加“「内存屏障」”来禁止指令重排序的,从而实现有序性。
「指令重排序」:指令重排序指的是JIT编译器、cpu处理器和jmm定义的多级缓存存储,在编译字节码和运行机器指令时,在不影响程序最终执行结果的情况下,会对原语句执行的顺序进行优化。jmm多级缓存会让语句的执行并不一定是按照正确的读写操作进行的。但是这些都是jmm所允许的操作。因此需要通过同步来禁止相关的指令重排序,如内存屏障。重排序通常是编译器或运行时环境为了优化程序性能而采取的对指令进行重新排序执行的一种手段。重排序分为两类:编译期重排序和运行期重排序,分别对应编译时和运行时环境。
「硬件层面的“内存屏障”」
「JMM层面的“内存屏障”」
总结一下,JVM的实现会在volatile读写前后均加上「内存屏障」,在一定程度上保证有序性:
上述内存屏障的插入策略是非常保守的,比如一个volatile的写操作后面需要加上StoreStore和StoreLoad屏障,但这个写volatile后面可能并没有读操作,因此理论上只加上StoreStore屏障就可以,的确,有的处理器就是这么做的。但JMM这种保守的内存屏障插入策略能够保证在任意的处理器平台,volatile变量都是有序的。
ynchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为「volatile无法保证操作的原子性」。通常来说,使用volatile必须具备以下2个条件:
也就是说需要确保这个操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。
文章定期同步在GitHub上,欢迎star:https://github.com/Bronya0/JavaBook