对Volatile的理解
volatile关键字是java的一个关键字,是可以实现对类的属性字段进行修饰的,主要是实现在多线程情况下,多个线程可以可以真正的共享该属性。
volatile关键字的特点:
-
可见性
-
非原子性
-
禁止指令重排序
一、数据可见性(数据可见性指的就是B线程能够获得A线程的操作后的新值)
在介绍volatile关键字的特点之前,首先有必要想说一下背景知识:
-
第一:在现代计算机中,存在一个问题就是,主内存与运算器之间的运算速度是有差异的,所以就引入了高速缓存,实现数据的高速传递;在多处理器系统中,就会出现每一个处理器都会对应一个高速缓存,并且这些处理器都会最终共享这个主内存,再加上各个处理器之间又没有关系,各个工作相称也有他自己的工作内存,这时就会出现缓存一致性问题(这个就需要缓存一致性协议来处理 : MESI 后面再说)。
-
第二:java内存模型有规定:所有的共享变量都是存储在主内存中的,如果是方法内的变量则不然,他是属于局部变量,是线程私有的。每一个线程都是有自己的工作内存的,这个工作内存的作用就是用于存储他从主内存读取到的变量的值,形成一个副本,放在工作内存中的,那工作线程每一次操作的时候都是对工作内存中的变量的副本进行修改,并不会与主内存做数据的读写,所以就会造成各个线程数据不一致的情况。
而这个volatile的作用就是用于解决在多线程中各个线程数据缓存不一致的问题,就只需要在修饰的类变量或者实例变量上添加一个volatile关键字即可。看似很简单其实volatile背后做了很多的事情。
首先,先看看,在多线程情况下,不使用volatile关键字的代码执行结果:public class Demone1 extends Thread { // while循环的执行标记 private boolean flag = false; public boolean getFlag() { return flag; } @Override public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } flag = true; System.out.println("flag:" + flag); } public static void main(String[] args) { Demone1 demone1 = new Demone1(); demone1.start();// 线程执行 while (true) { if (demone1.getFlag()) { System.out.println("输出"); } } } }
会发现,一直在这个就是死循环,也就说明这个Boolean变量设置的无效,在main线程中,读到的永远是false。
这个问题如何解决呢?
-
使用加锁实现。synchronized
public class Demone1 extends Thread { // while循环的执行标记 private boolean flag = false; public boolean getFlag() { return flag; } @Override public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } flag = true; System.out.println("flag:" + flag); } public static void main(String[] args) { Demone1 demone1 = new Demone1(); demone1.start();// 线程执行 while (true) { synchronized (String.class) { if (demone1.getFlag()) { System.out.println("输出"); } } } } }
使用锁synchronized同步锁可以实现数据的一致性,是因为线程在synchronized代码块执行前会获得锁对象,然后清空线程的工作内存,从主内存中刷新新的数据值到线程的工作内存中,当执行完毕后,会将新的值赋值给主内存,然后释放锁,以便下一个线程获得资源。
使用volatile关键字实现
public class Demone1 extends Thread { // while循环的执行标记 private volatile boolean flag = false; public boolean getFlag() { return flag; } @Override public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } flag = true; System.out.println("flag:" + flag); } public static void main(String[] args) { Demone1 demone1 = new Demone1(); demone1.start();// 线程执行 while (true) { if (demone1.getFlag()) { System.out.println("输出"); } } } }
执行结果:不断地输出:“输出”语句。说明volatile关键字确实是可以实现在多线程环境***享变量的数据可见性。
那么volatile关键字背后到底做了啥,能让这个数据达到可见性。
首先先说这个缓存一致性协议:
缓存一致性协议主要解决的是缓存一致性问题,之所以出现缓存一致性问题:就是因为各个工作线程由于自己把值缓存在了自己的工作内存中,每一次读写的时候,都只会对工作内存的数据做改变,不与主内存打交道,再加上各个线程之间不能访问对方的工作内存。所以缓存一致性协议的主要思路就是,当前的线程在对数据进行操作的时候,如果发现这个变量是共享的,就会发出通知,让其他缓存了这个共享变量的线程对这个变量的缓存行为设置为无效行为,那这个时候,其他的线程再次读写操作时,发现自己的缓存行是无效的,就只能在主内存中读新的数据。
那刚才说到了这个其他的线程会发现自己的缓存行为无效,那这是咋发现的?
这就要再说说一个概念:嗅探机制
啥是嗅探机制?就是每一个处理器都是会在数据总线上查询传递的数据,以此来判断自己的缓存航是否被改变,如果自己的缓存行内存地址被改变,那么就会把自己的缓存行数据设置为无效,就就只能从主内存中获取数据了。
嗅探机制会出现一个问题,就是总线风暴。
总线风暴简单的说就是:他会在主内存不断的实现嗅探机制,导致总线的带宽会达到峰值。
二、非原子性
所谓的原子性就是一次操作要么都成功,要么都失败!
volatile的操作非原子性的,特别是在,对于 i++ i-- 的问题上是会出现数据不安全的现象的。
如何解决呢?
-
要么就是加锁
-
要么就是使用原子类,比如:AtomicInteger
AtomicInteger原子类,他之所以能够实现数据的原子操作,是因为在源码层面,他使用到了CAS和UnSafe
后期,在写一篇关于CAS和UnSafe的文章
三、禁止指令重排
-
什么是重排序?
指令重排,就是对代码指令进行重新排序,主要是体现在编译器和处理器上。有的代码指令主要的时间短。有的代码指令需要的时间长,编译器为了能够提高CPU 的运行效率,会将执行时间短的指令都排在一起。但是值得说的是:虽然编译器代码的重排序优化,但是会带来一个问题,就是在并发的情况下,多个线程操作的不可见性。
-
而volatile关键字是禁止指令重排的
volatile关键字有自己的禁止规则:
第一:volatile前的指令可以指令重排。
第二:volatile后的指令可以指令重排。
第三:volatile前的指令不能排在volatile后。
第四:volatile后的指令不能排在volatile前。
-
而volatile关键字之所以能够禁止指令重排,是因为有一个重要的东西:内存屏障
Java编译器在生成指令时,会在适当的位置插上内存屏障,依次来达到禁止指令重排的作用。
对于volatile的写来说:会在volatile域的前后插入内存屏障。
对于volatile的读来说:会在volatile域的后面插入两个内存屏障。
顺便再说一下:
as-if-serial:不管怎么优化重排序,在单线程情况下,执行的结果不能被改变。
四、volatile与synchronized关键字的区别
-
volatile是用来修饰类变量和实例变量的,而synchronized关键字是用于修饰普通方法、静态方法、代码块。
-
volatile不能保证原子性,但是synchronized是可以保证原子性的。
-
volatile提供了数据的可见性,而synchronized是通过互斥机制来实现数据的安全性。volatile关键字也可以看成是一种轻量级的synchronized,如果在多线程并发的情况下,只是想对共享变量进行多线程赋值操作的话,使用volatile比较好一些。