1.volatile关键字简介
volatile 是 JVM 提供的轻量级的同步机制,volatile具有两个特性:可见性,有序性。不保证并发情况下的原子性。
2.volatile关键字的可见性,有序性
可见性是指一个线程修改了volatile变量的值,其他线程能够立即知道发生的变更。
内存屏障
volatile可见性是依靠内存屏障(Memory Bariers)来实现的,内存屏障是一类同步屏障指令,是编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作,以避免代码重排序,从而达成了其他线程可见的效果。
内存屏障的作用可分为两种:
- 内存屏障之前的所有写操作都要回写到主内存
- 内存屏障之后的所有读操作都能获得内存屏障之前的所有写操作的最新结果
当某个线程收到通知,去读取volatile修饰的变量的值的时候,线程私有工作内存的数据失效,需要重新回到主内存中去读取最新的数据,也叫做写后读,从而保证了volatile的可见性
细究内存屏障的工作流程
对应的内存屏障共四种,主要可分为两种,读屏障与写屏障。
-
读屏障让工作内存或CPU高速缓存当中的缓存数据失效,重新回到主内存中获取最新的数据。
-
处理器在写屏障之前将所有存储在缓存(storebufferes)中的数据同步到主内存。
(事实上内存屏障有四种,上面两种分别是load load bariers 和 store store bariers,还有load store baries 和 store loadbariers,但下面两个屏障的主要效果是保障上两个屏障正常运行,类似于innodb的插入意向锁的功能,故在此默认只有读屏障和写屏障,方便理解)
由于内存屏障前的内容是不可以重排到内存屏障后的,故对于volatile的写,发生于任何一个对于volatile的读之前,也叫做写后读,这样就保证了不同线程对于volatile变量的可见性。
3.volatile不具备原子性
原子性是指一个操作是不可打断的,即多线程环境下,操作不能被其他线程干扰。
volatile变量和普通变量一样都是不具备原子性的,下面是一个普通变量进行读写的过程。
在JMM(Java Memory Model)中定义了8中线程工作内存与主内存的原子操作:
这8个原子操作的功能如下:
- read:作用于主内存,将变量的值从主内存传输到工作内存,主内存到工作内存
- load:作用于工作内存,将read从主内存传输的变量值放入工作内存变量副本中,即数据加载
- use:作用于工作内存,将工作内存变量副本的值传递给执行引擎,每当JVM遇到需要该变量的字节码指令时会执行该操作
- assign:作用于工作内存,将从执行引擎接收到的值赋值给工作内存变量,每当JVM遇到一个给变量赋值字节码指令时会执行该操作
- store:作用于工作内存,将赋值完毕的工作变量的值写回给主内存
- write:作用于主内存,将store传输过来的变量值赋值给主内存中的变量由于上述6条只能保证单条指令的原子性,针对多条指令的组合性原子保证,没有大面积加锁,所以,JVM提供了另外两个原子指令:
- lock:作用于主内存,将一个变量标记为一个线程独占的状态,只是写时候加锁,就只是锁了写变量的过程
- unlock:作用于主内存,把一个处于锁定状态的变量释放,然后才能被其他线程占用
对于volatile变量(在JMM中和普通变量的操作相同),在工作内存write/load主内存时,主内存加锁,进入线程独占状态,又由于此时因为内存屏障的原因代码是有序的,所以读写volatile变量的过程是安全的。又由于解锁后,本线程会重新将主内存的值更新到工作内存,所以对于单个线程的volatile操作是安全的。
但是当多个线程同时操作一个volatile变量,此时一个线程独占主内存进行操作,读屏障只能保证其他线程读volatile变量时(load数据时)是最新的数据,当这些阻塞线程因加锁而等待后,独占线程修改数据而释放锁,这时这些阻塞线程并没有更新数据,故不能保证原子性(会受到其他线程的干扰)。
故volatile变量和普通变量一样,在并发环境下也需要手动加锁进行同步操作。