【并发与多线程】Volatile大总结

大佬们的总结文章:
第一篇(偏底层)
第二篇(偏理解)
第三篇(代码解释)
(PS:本文基于以上三篇文章)

一.为何有volatile

首先java内存模型(JMM)中,每一个线程有本身的工做内存,同时还有一个共享的主内存。以下图:
2.png
不直接让线程从主内存(MainMemory)中读取数据,是由于cpu的指令速度远超内存的存取速度,因此现代计算机系统都不得不加入一层读写速度尽量接近处理器运算速度的高速缓存(Cache)来做为内存与处理器之间的缓冲。也就是图中的工做内存。
基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,可是也为计算机系统带来更高的复杂度,由于它引入了一个新的问题:缓存一致性(CacheCoherence)。以下图:
1.png
假如说线程1修改了data变量的值为1,而后将这个修改写入本身的本地工做内存。那么此时,线程1的工做内存里的data值为1。然而,主内存里的data值仍是为0!线程2的工做内存里的data值也仍是0。html

这就是所谓的 java并发编程中的可见性问题
多个线程并发读写一个共享变量的时候,有可能某个线程修改了变量的值,可是其余线程看不到!也就是对其余线程不可见!

二.volatile的做用及背后的原理

要解决可见性问题,只须要用volatile修饰data。
以下代码:java

public class Solution {
  private static volatile int data = 0;
  public static void main(String[] args) {
    DataThread1 d1 = new DataThread1();
    DataThread2 d2 = new DataThread2();
    // 线程1读取和修改data变量值
    d1.run();
    // 线程2读取data变量值
    d2.run();
  }
}

这里说一下volatile的主要功能:编程

  • 第一,一旦data变量定义的时候前面加了volatile来修饰的话,那么线程1只要修改data变量的值,就会在修改完本身本地工做内存的data变量值以后,强制将这个data变量最新的值刷回主内存,必须让主内存里的data变量值立马变成最新的值!

3.png

  • 第二,若是此时别的线程的工做内存中有这个data变量的本地缓存,也就是一个变量副本的话,那么会强制让其余线程的工做内存中的data变量缓存直接失效过时,不容许再次读取和使用了!

4.png

  • 第三,若是线程2在代码运行过程当中再次须要读取data变量的值,此时尝试从本地工做内存中读取,就会发现这个data = 0已通过期了!此时,他就必须从新从主内存中加载data变量最新的值!那么不就能够读取到data = 1这个最新的值了!

5.png

小结:
对一个变量加了volatile关键字修饰以后,只要一个线程修改了这个变量的值,立马强制刷回主内存。
接着强制过时其余线程的本地工做内存中的缓存,最后其余线程读取变量值的时候,强制从新从主内存来加载最新的值!
这样就保证,任何一个线程修改了变量值,其余线程立马就能够看见了!这就是所谓的volatile保证了可见性的工做原理!缓存

volatile主要做用是保证可见性以及有序性。
有序性涉及到较为复杂的指令重排内存屏障等概念,本文没说起,可是volatile是不能保证原子性的安全

  • 也就是说,volatile主要解决的是一个线程修改变量值以后,其余线程立马能够读到最新的值,是解决这个问题的,也就是可见性!
  • 可是若是是多个线程同时修改一个变量的值,那仍是可能出现多线程并发的安全问题,致使数据值修改错乱,volatile是不负责解决这个问题的,也就是不负责解决原子性问题!
  • 原子性问题,得依赖synchronized、ReentrantLock等加锁机制来解决。

JMM主要就是围绕着如何在并发过程当中如何处理原子性、可见性和有序性这3个特征来创建的,经过解决这三个问题,能够解除缓存不一致的问题。而volatile跟可见性和有序性都有关。
(PS:三种特性看第三篇文章)多线程

三.volatile与synchronized区别

  • volatile只能修饰实例变量和类变量,而synchronized能够修饰方法,以及代码块。
  • volatile保证数据的可见性,可是不保证原子性(多线程进行写操做,不保证线程安全);而synchronized是一种排他(互斥)的机制。
  • volatile用于禁止指令重排序:能够解决单例双重检查对象初始化代码执行乱序问题。
  • volatile能够看作是轻量版的synchronized,volatile不保证原子性,可是若是是对一个共享变量进行多个线程的赋值,而没有其余的操做,那么就能够用volatile来代替synchronized,由于赋值自己是有原子性的,而volatile又保证了可见性,因此就能够保证线程安全了。

四.volatile的应用

状态量标记
int a = 0;
volatile bool flag = false;

public void write() {
    a = 2;              //1
    flag = true;        //2
}

public void multiply() {
    if (flag) {         //3
        int ret = a * a;//4
    }
}
单例模式中的应用
class Singleton{
    private volatile static Singleton instance = null;
 
    private Singleton() {
 
    }
 
    public static Singleton getInstance() {
        if(instance==null) {
            synchronized (Singleton.class) {
                if(instance==null)
                    instance = new Singleton();
            }
        }
        return instance;
    }
}

五.总结

  1. volatile修饰符适用于如下场景:某个属性被多个线程共享,其中有一个线程修改了此属性,其余线程能够当即获得修改后的值,好比boolean flag;或者做为触发器,实现轻量级同步。
  2. volatile属性的读写操做都是无锁的,它不能替代synchronized,由于它没有提供原子性和互斥性。由于无锁,不须要花费时间在获取锁和释放锁上,因此说它是低成本的。
  3. volatile只能做用于属性,咱们用volatile修饰属性,这样compilers就不会对这个属性作指令重排序。
  4. volatile提供了可见性,任何一个线程对其的修改将立马对其余线程可见,volatile属性不会被线程缓存,始终从主存中读取。
  5. volatile提供了happens-before保证,对volatile变量v的写入happens-before全部其余线程后续对v的读操做。
  6. volatile能够使得long和double的赋值是原子的。
  7. volatile能够在单例双重检查中实现可见性和禁止指令重排序,从而保证安全性。
相关文章
相关标签/搜索