volidate关键字在JDK中有大量应用,其主要作用有二个:1是保证变量在多线程之间的可见性;2是禁止指令重排序;但是volidate并不具备原子性。下面将分别介绍可见性、指令重排序、实现原理及为什么不能保证原子性。
可见性:可见性是指多线程情况下线程能够自动发现volatile变量的最新值。如果对Java内存模型比较了解的话会知道,每个线程都会被分配一个线程栈,如果对象是多线程间的共享资源时,当线程访问某一个对象时候值的时候,首先通过对象的引用找到对应在堆内存的变量的值,然后把堆内存变量的值load到线程栈中,建立一个变量副本,之后线程操作的都是副本变量,当修改完副本变量之后,会写回值到主内存。但由于线程栈是线程间相互隔离的,即多线程间不可见,如果有其他线程修改了这个变量,但还未写回主内存或者更新主内存后,其他线程读取的仍是自己线程栈的副本时,就会出现问题。而volidate则是用来保证可见性。即一个线程对共享变量的修改,能够及时被其他线程看到。
指令重排序:在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。虽然代码顺序是有先后顺序,但真正执行时却不一定按照代码顺序执行。这样在多线程下就可能存在问题。注意:只是可能出现问题,另外指令重排序在实际下发生情况比较少,由于Java、CPU和内存之间都有一套严格的指令重排序规则,具体可参照JSR和JVM相关资料。重排序分三种类型:
- 编译器优化的重排序:编译器在不改变单线程程序语义的前提下,重新安排语句的执行顺序。
- 指令级并行的重排序:现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
- 内存系统的重排序:由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
原理:
第一张图没有加volatile,第二张图加上volatile,对比汇编指令可以发现加上volatile后在对变量进行add操作时,会加上lock,通过lock指令形成内存屏障(具体可查询Lock指令和JVM相关资料),而内存屏障有三个作用:1是阻止屏障两边的指令重排序;2是强制把写缓冲区/高速缓存中的脏数据等写回主内存;3是如果是写操作,它会导致其他CPU中对应的缓存行无效。而具体如何做到第3点,则与MESI缓存一致性协议有关,这个属于硬件级别,利用的CPU总线嗅探机制,感兴趣者可自行查阅。
不保证原子性:原子性是指这个操作是不可中断,要么全部执行成功要么全部执行失败,就算在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰。volatile并不能保证原子操作,例如i++操作时,分为Load、Increment、Store、Memory Barriers四个步骤,即装载、新增、存储和内存屏障四个步骤,第四步则是保证jvm让最新的变量值在所有线程可见,但从Load、Increment、到Store是不安全的,中间如果其他的CPU线程修改值将会存在问题。
如何查看汇编指令:
- 将下载解压后的文件放入JDK所在目录下的jre/bin/server/下,下载地址:https://pan.baidu.com/s/1AmS2MCUygBihZxQmaU9vAw(提取码:3sri)
- 添加run configuration参数
-- 查看类全部指令:-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly
--查看指定方法:-server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:-Inline -XX:CompileCommand=print,*className.methodName
3. run 运行即可