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

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

一.为什么有volatile

首先java内存模型(JMM)中,每个线程有自己的工作内存,同时还有一个共享的主内存。如下图:
1.png
假如说线程1修改了data变量的值为1,然后将这个修改写入自己的本地工作内存。那么此时,线程1的工作内存里的data值为1。然而,主内存里的data值还是为0!线程2的工作内存里的data值也还是0。

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

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

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

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可以在单例双重检查中实现可见性和禁止指令重排序,从而保证安全性。

你可能感兴趣的:(多线程,volatile,并发,synchronized)