欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Volatile你真的懂了吗?

程序员文章站 2024-03-16 23:40:22
...

什么是volatile?

秒懂百科:volatile是一个特征修饰符(type specifier)

说到volatile很多人都知道它的功能有可见性、禁止指令重排序两大功能,那么如果再问你,它是怎么体现可见性,怎么禁止指令重排序的,以及在Java中哪些地方使用到他们,是如何实现的呢?

Volatile你真的懂了吗?

很显然这几个问题是有一定的难度的,如果面临这样的问题,我们最好先简单的解释一下volatile的作用,然后直接上手他在Java中自己知道的相关知识,这里知识有限,我也是给出了自己所了解的地方,希望有帮助!

Volatile你真的懂了吗?

首先:

可见性:

某一线程修改了某一共享变量的值,其他线程也能看到最新修改的值

禁止重排序:

编译器和处理器通常会对指令(指令是什么不过多介绍)做重排序,从而会造成一些线程并发场景下产生的问题,禁止重排序就能很好的控制这些问题的发生

下面通过两个例子来讲解这两个特性的实现:

1.可见性

前提了解:

线程要对主内存*享变量进行操作顺序是:先从主内存中copy一份该变量的副本到自己的工作内存中,进行修改后再把修改后的值更新至主内存

场景:

假设现在有两个线程A、B,他们都要对主内存中一共享变量 Number = 10进行操作,那么他们首先都会从主内存中copy一份副本至各自的工作内存,假设人为的将线程A睡眠3s 后执行一个方法将 Number = 10—修改—>Number =1024,并更新至主内存,而线程B一开始就会有一个自旋的While,用于判断Number = 10的话就一直自旋,不等于10就继续往下执行

实例代码:

package com.xiaozhao.juc;

import java.util.concurrent.TimeUnit;

/**
 * @author : Carson-Zhao
 * @date : 2020/7/12 18:00
 *
 *
 * JMM可见性
 *
 * 如果没有volatile修饰,通知main线程,程序则会在上面的While中一直循环,
 * 因为线程AAA修改了Number的值,但没有任何东西通知main
 *
 */
public class JMMDemo {
    public static void main(String[] args) throws InterruptedException {
        Data data = new Data();
        new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(3);
                data.add();
                System.out.println(Thread.currentThread().getName()+"====>"+data.Number);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"AAA").start();

        while (data.Number == 10){
            //3s后需要一种通知机制告诉main线程,Number已修改为1024,请跳出While—————应该做事儿
        }
        System.out.println(Thread.currentThread().getName()+"====>"+data.Number);
    }
}

class Data{

    int Number = 10;
    public void add(){

        this.Number = 1024;

    }
}

不用volatile时的输出:

线程会一直B一直会进行自旋,线程不停止,因为在它的工作内存中Number仍旧是10,没有人通知它A线程已将Number修改为1024了

Volatile你真的懂了吗?

使用volatile时输出:

volatile去通知Main线程,Number值已经被修改了,这时while就不成立了,就跳出了该循环,正常执行下面代码

Volatile你真的懂了吗?

总结:

可以说volatile起到的就是一个通知作用,使用它能够告知其他线程,它所有修饰的变量是不确定的、可变的,如果需要获取,每次都要从主内存中去进行获取,从而保证了该变量在内存中的可见性!

 

2.禁止重排序

一个简单的例子:

package com.xiaozhao.juc;

/**
 * @author : Carson-Zhao
 * @date : 2020/7/14 13:38
 */
public class VolatileDemo {
    public static void main(String[] args) {
        Object s = new Object();
    }
}

反编译后:

D:\Environment\Java\jdk1.8.0_251\bin\javap.exe -c com.xiaozhao.juc.VolatileDemo
Compiled from "VolatileDemo.java"
public class com.xiaozhao.juc.VolatileDemo {
  public com.xiaozhao.juc.VolatileDemo();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class java/lang/Object
       3: dup
       4: invokespecial #1                  // Method java/lang/Object."<init>":()V
       7: astore_1
       8: return
}

Process finished with exit code 0

我们定位到main方法中,在(左边)0的时候,通过指令进行一个new的操作,但是这里的new至少是一个半初始化的操作,假设此时我们将指令4和7进行调换,这就是Java中的重排序。那么此时,建立索引指向在二次初始化之前,此时S对象指向一个半初始化的对象。假设我们又来了一个线程对S进行判断,如果为空,则重新创建(一系列操作),如果不为空,则直接使用,在之前的场景下,第二个线程是会直接认为s是非空的一个对象,那么他就会拿着S去使用,但是此时他拿到的是一个半初始化的对象,显然这样的设计时不合理的,那么这个时候我们的volatile的作用就体现出来了!

它会禁止指令4和7进行调换(禁止重排序)保证S对象的两次初始化过程完整,从而保证其他线程过来使用S时,得到的是一个完整的对象

使用:

       volatile关键字在很多地方被使用,例如AQS中修饰State、双重双检单例模式以及一些对变量的写操作不依赖当前值和该变量没有包含在具有其他变量的不变式中等地方

最后:

       谢谢捧场!因为菜所以学,与君共勉!