Volatile详解

视频学习地址:尚硅谷Java大厂面试题第二季(java面试必学,周阳主讲)

谈谈对Volatile的理解
Volatile在日常的单线程环境是应用不到的
Volatile是Java虚拟机提供的轻量级的同步机制(三大特性)

  • 保证可见性
  • 不保证原子性
  • 禁止指令重排

JMM
JMM是Java内存模型,也就是Java Memory Model,简称JMM,本身是一种抽象的概念,实际上并不存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
JMM关于同步的规定:

  • 线程解锁前,必须把共享变量的值刷新回主内存
  • 线程加锁前,必须读取主内存的最新值,到自己的工作内存
  • 加锁和解锁是同一把锁

由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写会主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程:
Volatile详解_第1张图片
JMM内存模型的可见性,指的是当主内存区域中的值被某个线程写入更改后,其它线程会马上知晓更改后的值,并重新得到更改后的值。

缓存一致性
为什么这里主线程中某个值被更改后,其它线程能马上知晓呢?其实这里是用到了总线嗅探技术。

在说嗅探技术之前,首先谈谈缓存一致性的问题,就是当多个处理器运算任务都涉及到同一块主内存区域的时候,将可能导致各自的缓存数据不一。

为了解决缓存一致性的问题,需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议进行操作,这类协议主要有MSI、MESI等等。

MESI
当CPU写数据时,如果发现操作的变量是共享变量,即在其它CPU中也存在该变量的副本,会发出信号通知其它CPU将该内存变量的缓存行设置为无效,因此当其它CPU读取这个变量的时,发现自己缓存该变量的缓存行是无效的,那么它就会从内存中重新读取。

总线嗅探
那么是如何发现数据是否失效呢?

这里是用到了总线嗅探技术,就是每个处理器通过嗅探在总线上传播的数据来检查自己缓存值是否过期了,当处理器发现自己的缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置为无效状态,当处理器对这个数据进行修改操作的时候,会重新从内存中把数据读取到处理器缓存中。

总线风暴
由于Volatile的MESI缓存一致性协议,需要不断的从主内存嗅探和CAS循环,无效的交互会导致总线带宽达到峰值。
解决办法:部分volatile和cas使用synchronize

JMM的特性
JMM的三大特性,volatile只保证了两个,即可见性和有序性,不满足原子性

  • 可见性
  • 原子性
  • 有序性

Volatile保证可见性测试

import java.util.concurrent.TimeUnit;

//假设是物理内存
class MyData{
    int number=0;

    public void add(){
        this.number=60;
    }
}

//验证volatile的可见性
public class volatileTest01 {

    public static void main(String[] args) {
        MyData myData=new MyData();

        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"\t come in");

            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            myData.add();

            System.out.println(Thread.currentThread().getName()+"\t update number value:"+myData.number);
        },"AAA").start();

        while (myData.number==0){

        }

        System.out.println(Thread.currentThread().getName()+"\t mission is over");
    }

}

输出结果为:
Volatile详解_第2张图片
Volatile详解_第3张图片
Volatile不保证原子性测试

//假设是主物理内存
class MyData02{
    volatile int number =0;

    public void addPlus(){
        number++;
    }
}

public class volatileTest02 {

    public static void main(String[] args) {
        MyData02 myData=new MyData02();

        for(int i=0;i<20;i++){
            new Thread(()->{
                for(int j=0;j<1000;j++)
                    myData.addPlus();
            },String.valueOf(i)).start();
        }

        while (Thread.activeCount()>2){
            Thread.yield();
        }

        System.out.println(Thread.currentThread().getName()+"\t finally number value: "+myData.number);
    }
}

Volatile详解_第4张图片
看看字节码:
Volatile详解_第5张图片
解决方法:

  • synchronized
  • AtomicInteger

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

源代码 -> 编译器优化的重排 -> 指令并行的重排 -> 内存系统的重排 -> 最终执行指令

单线程环境里面确保最终执行结果和代码顺序的结果一致;

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

多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。

Volatile如何保证可见性
Volatile实现禁止指令重排优化,从而避免了多线程环境下程序出现乱序执行的现象。

首先了解一个概念,内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,它的作用有两个:

  • 保证特定操作的顺序
  • 保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)

由于编译器和处理器都能执行指令重排的优化,如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说 通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。 内存屏障另外一个作用是刷新出各种CPU的缓存数,因此任何CPU上的线程都能读取到这些数据的最新版本。

Volatile详解_第6张图片
也就是过在Volatile的写 和 读的时候,加入屏障,防止出现指令重排的。

参考链接:谈谈Volatile

你可能感兴趣的:(Java多线程,多线程,java)