volatile的学习总结

1.volatile是Java虚拟机提供的轻量级的同步机制

  • 保证可见性
  • 不保证原子性
  • 禁止指令重排
2. Java内存模型(JMM) JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过规范定义了程序中的各个变量(包括实例字段、静态字段和构成数组对象的元素)的访问方式。
JMM的同步规定:
  1. 线程解锁前,必须把共享变量的值刷新回主内存
  2. 线程加锁前,必须读取主内存的最新值到自己的工作内存
  3. 加锁解锁是同一把锁
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存时每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回到主内存,不能直接操作主内存中的变量,各个线程的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要的访问过程如下图:
volatile的学习总结
文章图片

JMM的三大特性
【volatile的学习总结】JMM是线程安全性获得的保证。因为JMM具有如下特点:
  1. 可见性:从主内存拷贝变量后,如果某一个线程在自己的工作内存中对变量进行了修改,然后写回了主内存,其它线程能第一时间看到,这就叫作可见性。
  2. 原子性:不可分割,完整性,也即某个线程正在做某个具体业务时,中间不可以被加塞或者被分割
  3. 有序性:禁止指令重排,按照规定的顺序去执行
综上所述,volatile满足JMM三大特性中的两个,即可见性和有序性,volatile并不满足原子性,所以说volatile是轻量级的同步机制。
3. 代码验证Volatile的可见性 代码示例:
/** * Created by salmonzhang on 2020/7/4. * 可见性代码实例 */ public class VolatileDemo { public static void main(String[] args) { MyData myData = https://www.it610.com/article/new MyData(); new Thread(() -> { System.out.println(Thread.currentThread().getName() + "\t come in ..."); //暂停一会儿线程 try{ TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } myData.addTo10(); System.out.println(Thread.currentThread().getName() + "\t update number value:" + myData.number); },"Thread01").start(); while (myData.number == 0) { //main线程一直在这里等待,直到number的值不再等于零 } System.out.println(Thread.currentThread().getName()+"\t mission is over , number updated ..."); } } class MyData{ //int number = 0; // 这里没有加volatile volatile int number = 0; // 这里加了volatile public void addTo10() { this.number = 10; } }

没有加volatile的运行结果:
volatile的学习总结
文章图片

加了volatile的运行结果:
volatile的学习总结
文章图片

总结:如果不加volatile关键字,则主线程会进入死循环,加了volatile时主线程运行正常,可以正常退出,说明加了volatile关键字后,当有一个线程修改了变量的值,其它线程会在第一时间知道,当前值作废,重新从主内存中获取值。这种修改变量的值,让其它线程第一时间知道,就叫作可见性。
4. 代码验证Volatile不保证原子性 代码示例:
/** * Created by salmonzhang on 2020/7/4. * 验证volatile不保证原子性 * 原子性是什么意思: * 不可分割,完整性,也即某个线程正在做某个具体业务时,中间不可以被加塞或者被分割。 * 需要整体完整,要么同时成功,要么同时失败。保证数据的原子一致性 */ public class VolatileDemo2 { public static void main(String[] args) { MyData2 myData2 = new MyData2(); for (int i = 1; i <= 20; i++){ new Thread(() -> { for (int j = 0; j < 1000; j++) { myData2.addPlusPuls(); } },String.valueOf(i)).start(); } //需要等待上面20个线程全部执行完成后,再用main线程取得最终的结果值看看是多少? while (Thread.activeCount() > 2) { //后台默认有两个线程:GC线程和main线程 Thread.yield(); } System.out.println(Thread.currentThread().getName() + "finally number value = "https://www.it610.com/article/+ myData2.number); } } class MyData2{ volatile int number = 0; // 这里加了volatile public void addPlusPuls() { number++; } }

运行结果:
volatile的学习总结
文章图片

从代码的运行结果会发现:会出现number最终的结果有可能出现不是20000的时候,这就证明了volatile不能保证原子性。
5. volatile不能保证原子性的原因和解决方案
  1. 为什么volatile不能保证原子性?
    由于多线程进程调度的关系,在某一时间段出现了丢失写值的情况。因为线程切换太快,会出现后面的线程会把前面的线程的值刚好覆盖。
    例如:Thread1和Thread2同时从主内存中读取number的值1到自己的工作内存,并同时进行了+1的动作,当Thread1将2写会主内存的时候,由于线程的调度原因,Thread2并没有第一时间知道Thread1已经将number的值改为了2,而是直接将Thread1改的number值进行覆盖,这样就会导致数据丢失。
  2. 解决方案:
    2.1. 直接在addPlusPuls前面加上synchronized
    class MyData2{ volatile int number = 0; // 这里加了volatile public synchronized void addPlusPuls() { number++; } }

    但是为了保证一个number++的原子性直接用synchronized,感觉有点重,类似于“杀鸡用牛刀”
    2.2 用atomic
    class MyData2{ AtomicInteger number = new AtomicInteger(); public void addPlusPuls() { number.getAndIncrement(); } }

7. 有序性
  1. 计算机在执行程序时,为了提高性能,编译器的处理器通常会对指令做重排,一般有三种重排:
    • 编译器的重排
    • 指令并行的重排
    • 内存系统的重排
volatile的学习总结
文章图片

  1. 单线程环境里确保程序最终执行的结果和代码执行的结果一致
  2. 处理器在进行重排序时,必须考虑指令之间的数据依懒性
  3. 多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证用的变量能否一致性是无法确定的,结果也是无法预测的
重排案例一:
public void mySort(){ int x=11; //语句1 int y=12; //语句2 x=x+5; //语句3 y=x*x; //语句4 }

计算机执行的顺序可能是:
1234
2134
1324
问题:
请问语句4可以重排后变成第一条码?
存在数据的依赖性,所以没办法排到第一个
重排案例二:
volatile的学习总结
文章图片

指令重排代码示例:
public class ReSortSeqDemo { int a = 0; boolean flag = false; public void method01() { a = 1; // 这里的a和flag没有禁止指令重排,所以在多线程环境中就有可能出现问题 flag = true; }public void method02() { if (flag) { a = a + 3; System.out.println("a = " + a); } } }

这里的a和flag没有禁止指令重排,所以在多线程环境中就有可能出现问题,例如指令重排后,method01中的flag=true先被Thread1执行了,此时Thread2又抢占到了线程资源去执行method02()时,此时的运行结果就是有问题的。运行结果就是a = 3,而不是正常情况下的a = 4
7. 单例模式下可能存在线程不安全 代码示例:
public class SingletonDemo { private static SingletonDemo instance = null; private SingletonDemo(){ System.out.println(Thread.currentThread().getName() + "\t 我是SingletonDemo的构造方法"); }; //synchronized 解决单例的多线程问题,会显得比较重,整个方法都被锁住了,不建议这么写 public static SingletonDemo getInstance(){ if (instance == null) { instance = new SingletonDemo(); } return instance; }public static void main(String[] args) { //并发多线程后,会出现构造函数多次执行的情况 for (int i = 1; i <= 10; i++){ new Thread(() -> { SingletonDemo.getInstance(); },String.valueOf(i)).start(); } } }

运行结果:
volatile的学习总结
文章图片

8. 单例模式下的volatile分析 1.代码示例:
public class SingletonDemo { private static volatile SingletonDemo instance = null; //加上volatile,禁止编译器指令重排 private SingletonDemo(){ System.out.println(Thread.currentThread().getName() + "\t 我是SingletonDemo的构造方法"); }; /** * DCL (double check Lock 双端检索机制) */ public static SingletonDemo getInstance(){ if (instance == null) { synchronized (SingletonDemo.class) { if (instance == null) { instance = new SingletonDemo(); } } } return instance; }public static void main(String[] args) { //并发多线程后,会出现构造函数多次执行的情况 for (int i = 1; i <= 10; i++){ new Thread(() -> { SingletonDemo.getInstance(); },String.valueOf(i)).start(); } } }

总结:
  • 如果没有加 volatile 就不一定是线程安全的,原因是指令重排序的存在,加入 volatile 可以禁止指令重排。
  • 原因是在于某一个线程执行到第一次检测,读取到的 instance 不为 null 时,instance 的引用对象可能还没有完成初始化。
  • instance = new Singleton() 可以分为以下三步完成
    memory = allocate(); // 1.分配对象空间 instance(memory); // 2.初始化对象 instance = memory; // 3.设置instance指向刚分配的内存地址,此时instance != null

  • 步骤 2 和步骤 3 不存在依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种优化是允许的。
  • 发生重排
    memory = allocate(); // 1.分配对象空间 instance = memory; //3.设置instance指向刚分配的内存地址,此时instance != null,但对象还没有初始化完成 instance(memory); // 2.初始化对象

  • 所以不加 volatile 返回的实例不为空,但可能是未初始化的实例
非常感谢您的耐心阅读,希望我的文章对您有帮助。欢迎点评、转发或分享给您的朋友或技术群。

    推荐阅读