1.volatile关键字介绍
1.1内存模型的相关概念
计算机在执行程序时,每条指令都是在CPU中执行,而在执行指令过程中,就会涉及到数据的读取和写入的问题,由于程序运行过程中的临时数据是存放在主存中的,这时就存在一个问题,由于CPU执行速度很快,而从内存中读取数据和向内存中写入数据的过程跟CPU指令执行的速度比起来要慢得多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度,因此在CPU里面就有了高速缓存
也就是当程序在运行过程中,会将运算所需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存中读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存中。
高速缓存存在一个问题,那就是如果一个共享变量在多个CPU中都存在缓存,那么就有可能出现缓存不一致问题,通常有两种解决方法:
- 通过在总线加LOCK锁的方式
- 通过缓存一致性协议
这两种方式都是硬件层面上提供的方式
对于第一种方法,是在总线上加LOCK锁的形式来解决缓存不一致问题,因为CPU和其他部件进行通信都是通过总线来进行的,如果对总线加LOCK锁的话,也就是说阻塞了其他CPU对其他不见访问,从而使得只能有一个CPU使用这个变量的内存
第二种方法:缓存一致性协议,比较有代表性的就是MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的,它的核心思想就是:当CPU写数据时,如果发现操作的变量是共享变量,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量是无效的,那么它就会从内存中重新读取
对于可见性(当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立刻看得到修改的值),Java提供了volatile关键字来保证可见性和禁止指令重排
1.1.1指令重排序
这里解释一下什么是指令重排序,一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序一致。处理器在进行重排序时是会考虑指令之间的数据依赖性的,在单线程中,数据依赖性很强,所以指令重排序不会影响单个线程的执行,但是对于多线程,可能会影响到线程并发执行的正确性
1.2 volatile关键字详解
当一个共享变量被volatile修饰时,它会保证修改的值会立即更新到主存中,当有其他线程需要读取时,它会去内存中读取新值,另外通过synchronized和LOCK锁也能够保证可见性,synchronized和LOCK保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码
在Java内存模型中具备一些先天的”有序性“,即不需要通过任何手段就能够得到保证的有序性,这个也通常称为happens-before原则,如果两个操作的执行次序无法从happens-before原则推导出来,那么他们就不能保证他们的有序性,虚拟机可以随意对他们进行重排序
1.2.1happens-before原则
- 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
- 锁定规则:一个unLock(释放锁)操作线性先行发生于后面对同一锁的lock操作
- volatile变量规则:对于一个变量的写操作先行发生于后面对这个变量的读操作
- 传递规则:如果操作A先行发生于操作B,操作B先行发生于操作C,则可以得出操作A先行发生于操作C
- 线程启动规则:一个线程的start()方法先行发生于此线程的每一个动作
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
- 线程终结规则:线程中所有的操作都先行发生于线程的终止检测
- 对象终结规则
1.2.2volatile关键字的两层语义
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰后,那么就具备了两层语义:
- 保证了不同线程对这个变量进行操作时的可见性
- 禁止进行指令重排序
当我们对一个共享变量加入volatile关键字时会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障,内存屏障会提供三个功能:
- 它确保指令重排序时不会把其后面指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障后面;即在执行到内存屏障这句指令时,在它前面的操作就已经全部完成
- 它会强制将对缓存的修改操作立即写入主存中
- 如果是写操作,它会导致其他CPU对应的缓存行无效
2.Java中能创建volatile数组吗
Java中可以创建volatile数组,不过只是一个指向数组的引用,意思时,如果该改变引用指向的数组,那么会受到volatile的保护,但是如果多个线程同时改变数组的元素,volatile标识符就不能起到之前的保护作用了
3.volatile使用条件
只能在有限的一些情形下使用volatile变量替代锁,要使volatile变量提供理想的线程安全,必须同时满足下面两个条件:
- 对变量的写操作不依赖于当前值,即当变量的值由自身的上一个决定时,如n=n+1,n++等,volatile关键字将失效
- 该变量没有包含在具有其他变量的不变式中
4.synchronized和volatile的区别是什么
synchronized表示只有一个线程可以获取作用对象的锁,执行代码,阻塞其他线程,volatile表示变量在CPU的寄存器中是不确定的,必须从主存中读取,保证多线程环境下的可见性,禁止指令重排序
区别:
- volatile是变量修饰符,synchronized可以修饰类,方法,变量
- volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
- volatile不会造成线程的阻塞,synchronized可能会造成线程的阻塞
- volatile标记的变量不会被编译器优化(因为volatile禁止指令重排序),而synchronized标记的变量可以被编译器优化
- volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好