前言
volatile是Java虚拟机提供的轻量级的同步机制。
volatile关键字作用是什么?
两个作用:
1.保障被volatile润饰的共享变量对所有线程总数可见的,也就是当一个线程批改了一个被volatile润饰共享变量的值,新值总是能够被其余线程立刻得悉。
2.禁止指令重排序优化。
volatile的可见性
对于volatile的可见性作用,咱们必须意识到被volatile润饰的变量对所有线程总数立刻可见的,对volatile变量的所有写操作总是能立即反馈到其余线程中;
上面来测试一下,此时的还未initFlag
被volatile润饰。
private boolean initFlag = false; public void test() throws InterruptedException{ Thread threadA = new Thread(() -> { while (!initFlag) { } String threadName = Thread.currentThread().getName(); System.out.println("线程" + threadName+"获取到了initFlag扭转后的值"); }, "threadA"); //线程B更新全局变量initFlag的值 Thread threadB = new Thread(() -> { initFlag = true; }, "threadB"); //确保线程A先执行 threadA.start(); Thread.sleep(2000); threadB.start();}
执行后果:控制台只打印了 "线程threadB扭转了initFlag的值",且程序并未终止。
此时initFlag曾经被volatile关键字润饰了
private volatile boolean initFlag = false; public void test() throws InterruptedException{ Thread threadA = new Thread(() -> { while (!initFlag) { } String threadName = Thread.currentThread().getName(); System.out.println("线程" + threadName+"获取到了initFlag扭转后的值"); }, "threadA"); Thread threadB = new Thread(() -> { initFlag = true; String threadName = Thread.currentThread().getName(); System.out.println("线程" + threadName+"扭转了initFlag的值"); }, "threadB"); //确保线程A先执行 threadA.start(); Thread.sleep(2000); threadB.start();}
执行后果:
线程threadB扭转了initFlag的值
线程threadA获取到了initFlag扭转后的值
并且程序曾经完结了。
这个案例充分说明了volatile的可见性作用。
volatile无奈保障原子性
来个案例阐明所有:
private static volatile int count = 0;/** * count尽管被volatile关键字润饰,然而后果并不是50000,而是小于等于50000 **/public static void main(String[] args) throws InterruptedException{ //开启10个线程,别离对count进行自增操作 for (int i = 0; i < 10; i++) { Thread thread = new Thread(() -> { for (int j = 0; j < 5000; j++) { count++; //先读,再加,不是一个原子操作 } }); thread.start(); } Thread.sleep(2000); System.out.println("count==" + count);}
count尽管被volatile关键字润饰了,然而输入的后果会小于等于50000,足以阐明了volatile无奈保障原子性。
volatile禁止重排优化
volatile关键字另一个作用就是禁止指令重排优化,从而防止多线程环境下程序呈现乱序执行的景象。
内存屏障,又称内存栅栏,是一个CPU指令,它的作用有两个,一是保障特定操作的执行程序,二是保障某些变量的内存可见性(利用该个性实现volatile的内存可见性)。因为编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会通知
编译器和CPU,不论什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。Memory Barrier的另外一个作用是强制刷出各种CPU的缓存数据,因而任何CPU上的线程都能读取到这些数据的最新版本。
总之,volatile变量正是通过内存屏障实现其在内存中的语义,即可见性和禁止重排优化。
上面看一个十分典型的禁止重排优化的例子,如下:
//禁止指令重排优化private volatile static VolatileSingleton singleton; public static VolatileSingleton getInstance(){ if(singleton != null){ synchronized (VolatileSingleton.class){ if(singleton != null){ //多线程环境下可能会呈现问题的中央 singleton = new VolatileSingleton(); } } } return singleton;}
新new一个对象是分为三步来实现:
memory = allocate();//1.调配对象内存空间
instance(memory);//2.初始化对象
singleton = memory;//3.设置singleton对象指向刚调配的内存地址,此时singleton != null
因为步骤1和步骤2间可能会重排序,如下:
memory = allocate();//1.调配对象内存空间
singleton = memory;//3.设置singleton对象指向刚调配的内存地址,此时singleton != null
instance(memory);//2.初始化对象
因为步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行后果,在单线程中并没有扭转,因而这种重排优化是容许的。然而指令重排只会保障串行语义的执行的一致性(单线程),但并不会关怀多线程间的语义一致性。所以当一条线程拜访singleton不为null时,因为singleton实例未必已初始化实现,也就造成了线程平安问题,volatile禁止singleton变量被执行指令重排优化.
volatile重排序规定表
能够总结为三条:
- 当第二个操作是volatile 写时,不论第一个操作是什么,都不能重排序。这个规定确保volatile 写之前的操作不会被编译器重排序到volatile 写之后。
- 当第一个操作是volatile 读时,不论第二个操作是什么,都不能重排序。这个规定确保volatile 读之后的操作不会被编译器重排序到volatile 读之前。
- 当第一个操作是volatile 写,第二个操作是volatile 读时,不能重排序。
最初
感激你看到这里,看完有什么的不懂的能够在评论区问我,感觉文章对你有帮忙的话记得给我点个赞,每天都会分享java相干技术文章或行业资讯,欢送大家关注和转发文章!