volatile是什么

1,019 阅读7分钟

整理完再回头看的时候心里还是有一下几个疑问: 

 JMM与并发三个特性是什么关系?

 高速缓存L1 L2 L3 与主内存是什么关系?
 *** 高速缓存L1 L2 L3与主内存是独立的(高速的)存在,高速缓存
 使用SRAM技术实现,主内存使用DRAM技术实现。高速缓存是CPU与主内存之间的一个缓冲,降低CPU
 读写数据的时间(先从高速缓存读,没有才去主内存) ***

 工作内存与高速缓存L1 L2 L3是什么关系? 
 *** 可以简单理解为工作内存是对cache和register的抽象,主内存是对主存(内存条)的抽象 ***

隔了一天,今天在知乎上找到了一边针对JMM的详细的介绍 
建议先看看这个链接对JMM的详细介绍 + 多次

zhuanlan.zhihu.com/p/29881777

并行与并发

1.并发是指多个线程访问同一个资源(例如秒杀)
2.并行同时做多个事情

--通俗解释,参考 https://www.jianshu.com/p/cbf9588b2afb

volatile是什么

volatile是由JVM提供的轻量级的同步机制
volatile保证可见性和有序性(禁止指令重排序),但不保证原子性

JMM

(Java内存模型,Java Memory Model,简称JMM)

1.JMM是抽象的概念,并不真实存在,它描述的是一组规则或规范,
它定义了程序中的各个变量(实例变量、类变量和数组中的元素)的访问方式

2.JMM关于同步的规定:
    > 线程加锁前,必须读取主内存的最新值到自己的工作内存
    > 线程解锁前,必须把共享变量的值刷新回主内存
    > 加锁、解锁必须是同一把锁

------------------------------------------------------------------------------------------------

并发特性-可见性

    由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(也称栈空间)工作内存是每个线程私有的内存区域。

JMM规定所有变量都存储在主内存主内存是共享内存区域,所有线程都可以访问。

但线程对变量的读写必须在工作内存中进行。首先将变量从主内存拷贝到自己的工作内存中,然后对变量进行操作,操作完成后再蒋变量写回主内存,不能直接操作主内存中的变量。各个线程中的工作内存存储着主内存中的变量副本,某个线程对自己的变量副本的操作 其他线程默认是不会知道的,这就是内存可见性。JMM下共享变量的访问过程如下图:

volatile保证可见性验证

/**
 * <h1>验证volatile可见性</h1>
 * 共享变量 num 不使用volatile修饰 没有可见性
 * ------------------------------------
 * 共享变量 num 使用volatile修饰 保证可见性
 * num被修改后会马上刷新主内存并通知其他线程重新读取
 */
public class VolatileVisibilityDemo {    public static void main(String[] args) {
        MyData myData = new MyData();

        // 第一个线程 更新num = 60
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t come in");
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            myData.addTo60();
            System.out.println(Thread.currentThread().getName() + "\t update num: " + myData.num);
        }, "AAA").start();

        // 第二个线程就是main线程
        while (myData.num == 0) {
            // main线程一直等待循环 直到num值不等于0
            System.out.println(Thread.currentThread().getName() + "\t 不知道有没有其他线程对num的修改,一直空转");
        }
        System.out.println(Thread.currentThread().getName() + "\t 有其他线程对num的修改,退出空转," +
                "结束 num = " + myData.num);
    }
}

class MyData {

    volatile int num = 0;

    void addTo60() {
        num = 60;
    }
}

根据JMM的规范,各个线程对于主内存中共享变量都是要先拷贝到自己的工作内存,然后对拷贝的副本进行读写然后再写回到主内存。

假如有一个线程A修改了共享变量X的值,但是还没有写回主内存。此时另外一个线程B对主内存中的共享变量X进行了回写,B线程的回写操作对于线程A来说并不可见,这种工作内存与主内存同步延迟现象就造成了可见性问题。

------------------------------------------------------------------------------------------------

并发特性-原子性

原子性指的是:不可分割,也就是作为一个整体,要么全部执行,要么不执行。
简而言之,其他线程要么看到还没有执行的结果,要么看到执行完的结果,永远不会看到执行一半的结果

volatile不保证原子性验证

 下面例子中的 num++操作 在class文件中实际上是以下四行汇编指令

getfield    拿到原始值
icoonst_1        
iadd        执行加1操作
putfield    把累加的新值写回主内存

多个线程进行num++操作时,某些线程执行putfield指令回写主内存时可能会被中断,

在这些线程中断的时间窗口内(纳秒级别)可能有其他线程执行putfield指令回写了新值到主内存,

但是还没来得及通知原先中断的线程重新读取,

原先中断的线程就被CPU唤醒继续执行putfield指令,造成对主内存num变量的写覆盖,最终所有线程执行完num的最终值不是正确的(大部分情况下< 20000)

/**
 * <h1>验证volatile不保证原子性</h1>
 * 原子性指的是:不可分割,也就是作为一个整体,要么全部执行,要么不执行,不可以被中断。
 * 简而言之,其他线程要么看到还没有执行的结果,要么看到执行完的结果,永远不会看到执行一半的结果
 */
public class VolatileAtomicityDemo {
    public static void main(String[] args) {
        MyData1 myData1 = new MyData1();

        // 20个线程 每个线程对num累加1000 如果能保证原子性 num = 20000
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    myData1.increase();
                }
            }, "thread-" + i).start();
        }

        // 默认后台有main线程 + gc线程
        // 等待上面20个线程执行完
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }

        // 查看num最终值是多少
        System.out.println(Thread.currentThread().getName() + "\t 结束 num = " + myData1.num);
    }
}

class MyData1 {

    volatile int num = 0;

    void increase() {
        num++;
    }
}

volatile不保证原子性问题解决

1.JUC包下面的AtomicInteger原子类
2.JUC包下面的Lock接口
3.Synchronized关键字

------------------------------------------------------------------------------------------------

并发特性-有序性【指令重排序】

计算机在执行程序时时,为了提高性能,编译器处理器常常会对指令进行重排序,一般分为一下三种:

单线程环境里面指令重拍后程序最终执行结果和代码顺序执行的结果一致是得到保障的。

,***处理器在进行指令重排序必须要考虑指令间的数据依赖性***

但是多线程交替执行,由于指令重排序的存在,2个线程中使用的变量能否保持一致性是无法确定的,结果无法预测

举个栗子1:

int x = 11;//语句1
int y = 12;//语句2
x = x + 5; //语句3
y = x * x; //语句4

问题1: 可以有哪些执行顺序是不会影响最终结果的?
1234,2134,1324这三种顺序

问题2: 语句4可以重排变成第一个执行么?
不可以,语句4 变量y依赖变量4,需要在x完成赋值后才可用执行语句4

volatile 禁止指令重排序

volatile 通过 内存屏障(Memory Barrier,也称内存栅栏,CPU指令) 实现禁止指令重排序

内存屏障有2个作用
    > 保证指令的有序执行 禁止重排序
    > 保证某些变量的内存可见性(利用该特性实现了volatile的可见性)

由于编译器和处理器都能执行指令重拍优化。如果在指令间**(比如上面栗子中的语句4与语句3之间)插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说***通过插入内存屏障禁止在内存屏障前后的执行执行重排序优化**。

内存屏障还有另外一个作用: 强制更新各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新值

------------------------------------------------------------------------------------------------参考来源:[www.bilibili.com/video/BV1zb… - p9)