多线程的执行时,每条指令都是由CPU去执行的,程序运行中的临时数据存于主存(main memory)中,当从内存直接进行数据读写的时CPU执行效率会很低,所以就有了高速的缓存区—线程工作内存(working memory)也就是jvm中的虚拟机栈(本地方法栈用来处理native方法,后面会在jvm章节中提到)。
在处理数据时,线程会自动的把数据从主存中copy到线程工作内存中,当完成所有的操作后再刷新数据存储到主存中。
总体来说volatile有两大特性:
- volatile保证可见性:volatile关键字修饰的变量,线程在每次使用的时候都会刷新并读取最新的值,也就是说volatile会保证从主存加载到线程工作内存的值是最新的,但是volatile并不能保证原子性,是线程不安全的。比如:(i=1)进行i++操作,i++操作分为3步,先从主存中拿到i,再加1,最后将值赋给i,由于是volatile修饰所以能保证i是最新的即第一步的i是最新的,但是加1的时候却不能保证原子性,因为再计算加1时可能有其他线程已经开始运行并拿到i的值,而此时i最新的值还是1,所以当第一个线程加1后并更新i为2,而第二个线程第一步已经结束i的值已经取了,此时加1再赋值也是2,这就说明不能保证线程安全。
也就是大家说的:“对于volatile修饰的变量,JVM虚拟机只能保证从主内存加载到线程工作内存的值是最新的。”
所以当多线程中既有读又有写操作时,使用volatile可以保证此时读取的数是最新的,而不需要进行加锁操作,比如:ConcurrentHashMap的读操作。
2.volatile防止指令重排
指令重排是指JVM在编译java字节码的时候,会对现有的指令顺序进行重新排序,优化提高程序运行的效率。
比如: public void sort(){
int a =1;
int b =2;
x= x+5;
}
指令重排可能先执行 int a = 0,int b=0,x=x+5,再执行b的赋值 b=2;
这样就可能在多线程里面出现错误。
为了解决指令重排,可以使用内存屏障(Memory Barrier)
当在代码中使用了volatile之后,JVM会为我们做两件事:
1.在每个volatile写操作前插入StoreStore屏障,在写操作后插入StoreLoad屏障。
2.在每个volatile读操作前插入LoadLoad屏障,在读操作后插入LoadStore屏障。
这个时候的代码就相当于:
int a =1;
StoreLoad屏障
int b =2;
StoreLoad屏障
x= x+5;
这样 int a与int b就不能交换顺序,从而不执行指令重排,也就达到了happends-before的目标。