视频课程
小黑屋思过中,禁止观看!
评论并刷新后可见

您需要在视频最下面评论并刷新后,方可查看完整视频

视频课程
立即观看
付费视频

您支付费用,方可查看完整视频

¥{{user.role.value}}
课程视频
开始学习
会员专享

视频合集

volatile的实现原理,源码案例深度剖析!

  • 课程笔记
  • 问答交流

上一节讲到Java内存模型用来屏蔽不同硬件和操作系统的内存访问差异,期望Java程序在各种平台上都能实现一致的内存访问效果。

而具体在Java内存模型里是如何来解决内存数据一致性的问题呢?

这就不得不谈到今天的核心关键点:Volatile的实现原理。

为了助大家掌握好Volatile,本节课重点会讲到以下5点:

1.Volatile关键字

2.Java内存模型

3.Volatile内存模型可见性

4.Volatile的工作原理

5.Volatile的源码案例

在谈Volatile之前,我们先回顾下Java内存模型的三要素:原子性、可见性、有序性,也就是大家常提到的并发编程三要素。

 

并发编程的三要素

1.原子性

和数据库事务中的原子性一样,满足原子性特性的操作是不可中断的,要么全部执行成功要么全部执行失败

只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。

比如:i = 2;j = i;i++;i = i + 1;

上面4个操作中,i=2是读取操作,必定是原子性操作,j=i你以为是原子性操作,其实吧,分为两步,一是读取i的值,然后再赋值给j,这就是2步操作了,称不上原子操作,i++和i = i + 1其实是等效的,读取i的值,加1,再写回主存,那就是3步操作了。

所以上面的举例中,最后的值可能出现多种情况,就是因为满足不了原子性。

非原子操作都会存在线程安全问题,需要我们使用同步技术(sychronized)来让它变成一个原子操作,java的concurrent包下提供了一些原子类:比如:AtomicInteger、AtomicLong等。

2.可见性

多个线程访问同一个共享变量时,其中一个线程对这个共享变量值的修改,其他线程能够立刻获得修改以后的值

3.有序性

编译器和处理器为了优化程序性能而对指令序列进行重排序,也就是你编写的代码顺序和最终执行的指令顺序是不一致的。

但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

Volatile

Volatile 是一个Java语言的类型修饰符,一旦一个共享变量(类的成员变量、类的静态成员变量)被Volatile修饰之后,那么就具备了两层语义:

1、保证多线程下的可见性

2、禁止进行指令重排序(即保证有序性)

这里需要注意一个问题,Volatile只能让被他修饰内容具有可见性、有序性

Volatile只能保证对单次读/写的原子性,i++ 这种操作不能保证原子性。

Volatile的内存模型

Java 内存模型(JMM)是一种抽象的概念,并不真实存在,它描述了一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段、静态字段和构成数组对象的元素)的访问方式。

试图屏蔽各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的内存访问效果。

Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。

volatile的实现原理,源码案例深度剖析!-mikechen
  • 主内存主要存储的是Java实例对象,所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量还是方法中的本地变量(也称局部变量),当然也包括了共享的类信息、常量、静态变量。由于是共享数据区域,多条线程对同一个变量进行访问可能会发现线程安全问题。
  • 工作内存每条线程都有自己的工作内存(Working Memory,又称本地内存,可与前面介绍的处理器高速缓存类比),线程的工作内存中保存了该线程使用到的变量的主内存中的共享变量的副本拷贝。工作内存是 JMM 的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。

主要存储当前方法的所有本地变量信息(工作内存中存储着主内存中的变量副本拷贝),每个线程只能访问自己的工作内存,即线程中的本地变量对其它线程是不可见的,就算是两个线程执行的是同一段代码,它们也会各自在自己的工作内存中创建属于当前线程的本地变量,当然也包括了字节码行号指示器、相关Native方法的信息。

Volatile的实现原理

Volatile 保证内存可见性

volatile的实现原理,源码案例深度剖析!-mikechen

主内存和工作内存之间的交互有具体的交互协议,JMM定义了八种操作来完成,这八种操作是原子的、不可再分的,它们分别是:lock,unlock,read,load,use,assign,store,write,其中lock,unlock,read,write作用于主内存;load,use,assign,store作用于工作内存。

(1) lock:将主内存中的变量锁定,为一个线程所独占

(2) unclock:将lock加的锁定解除,此时其它的线程可以有机会访问此变量

(3) read:将主内存中的变量值读到工作内存当中

(4) load:将read读取的值保存到工作内存中的变量副本中。

(5) use:将值传递给线程的代码执行引擎

(6) assign:将执行引擎处理返回的值重新赋值给变量副本

(7) store:将变量副本的值存储到主内存中。

(8) write:将store存储的值写入到主内存的共享变量当中。

  • 从主存复制变量到当前工作内存(read and load)
  • 执行代码,改变共享变量值 (use and assign)
  • 用工作内存数据刷新主存相关内容 (store and write)

指令规则

  • read 和 load、store和write必须成对出现
  • assign操作,工作内存变量改变后必须刷回主内存
  • 同一时间只能运行一个线程对变量进行lock,当前线程lock可重入,unlock次数必须等于lock的次数,该变量才能解锁。
  • 对一个变量lock后,会清空该线程工作内存变量的值,重新执行load或者assign操作初始化工作内存中变量的值。
  • unlock前,必须将变量同步到主内存(store/write操作)

Volatile源码案例

package com.yzxy.concurrent.basic;


public class VolatileDemo extends  Thread{

    /**
     * 加与不加volatile
     * 不加volatile:main线程中将isRunning设置为flase,VolatileDemo线程中的isRunning不会改变
     *
     * 加上volatile:main线程中将isRunning设置为flase,VolatileDemo线程中的isRunning会随之改变
     */

    private    boolean  isRunning = true;


    private void setRunning(boolean isRunning){
        this.isRunning = isRunning;
    }


    @Override
    public void run(){
        System.out.println("进入run()方法...");

        while(isRunning){

            //如果VolatileDemo线程的isRunning不改为false,线程会永远卡在这里

        }

        System.out.println("线程停止!!!");
    }



    public static void main(String[] args) throws InterruptedException {

        VolatileDemo volatileDemo = new VolatileDemo();

        volatileDemo.start();
        Thread.sleep(1000);
        volatileDemo.setRunning(false);
        System.out.println("isRunning的值已经设置为false...");
    }


}
评论交流
  1. 会会

    while(isRunning){
    //如果VolatileDemo线程的isRunning不改为false,线程会永远卡在这里
    System.out.println(“running”) //加上这句话就不会死循环,怎么解释
    }

    • mikechen

      这个问题我记得面试辅导的时候就问过,面试辅导的时候回复了你,怎么还卡在这里。
      既然你发现了System.out.println(“running”) 加上这句话就不会死循环(volatile内存可见性失效),那解决的思路就很简单了:
      肯定是System.out.println这个方法让内存发生了可见性(替换了volatile的作用)。
      所以,解决思路就来了:看看println究竟做了什么事情。
      你只需要点一下println(进入源码),答案就放在那里。

    • 会会

      println 方法中是同步代码块, 同步代码块还会让不在代码块中的变量也失效?

    • mikechen

      直接说你的结论:既然你已经看了源码,也发现了同步代码块没有包含你的变量。

  2. 会会

    public class VolatileTest extends Thread {

    private boolean flag = false;

    public void setFlag(boolean flag) {
    this.flag = flag;
    }

    @Override
    public void run() {
    while (!flag){
    // System.out.println(1);
    }
    System.out.println(“线程停止”);
    }

    public static void main(String[] args) throws InterruptedException {
    VolatileTest test = new VolatileTest();
    test.start();
    Thread.sleep(1000);

    new Thread(new Runnable() {
    @Override
    public void run() {
    test.setFlag(true);
    }
    }).start();

    }
    }

    while循环体中,执行System.out.println方法, flag没有使用volatile修饰,线程2修改flag变量,代码正常退出
    执行main方法控制台打印如下

    1
    1
    1
    1
    1
    线程停止

  3. JansenZhang

    valatile在锁的实现上比如ReentrantLock中使用过,主要用来保证变量在多线程访问的情况下的可见性和有序性。volatile通过内存屏障来实现,在对变量的load和store时会增加内存屏障,内存屏障指令会告知jvm编译器和cpu,禁止前后的指令进行重新排序,保证了有序性;另外在store后的store屏障会立即将工作内存中的变量值刷新到主内存中,保证了可见性。

  4. 路正银

    volatile具有修改可见性(一个线程去修改值,别的线程是可见的),不具有原子性

    volatile的适用场景;状态标志、一次性安全发布、独立观察、“volatile bean” 模式、开销较低的“读-写锁”策略

    volatile的实现原理:
    vilatile可见性的实现是借助了CPU的lock指令,通过在写volatile的机器指令钱加上lock前缀,
    使写voletile具有以下两个原则:
    1、写volitile时处理器会将缓存写回到主内存
    2、一个处理器的缓存写回到内存会导致其他处理器的缓存失效。
    volitile有序性的保证是通过禁止指令重排序来实现的(内存屏蔽)

    • mikechen

      每一次作业的输出,都是一次非常好的面试的演练,其实就是一次线下面试的模拟演练。
      这里给到一个我的建议:可以先输出,输出前用语言的方式来表达出来,比如:我如果用语言的方式来表达,我应该怎么来回答volatile的实现原理,是否可以先抓住最核心的重点来切入:可见性与有序性,然后分别从可见性与有序性来解答这个角度,面试官一般给到的时间非常短(1-2分钟),能快速说重点,再从重点来阐述,这个角度面试官对你的印象就会非常好。所以,作业的输出,你完全可以按照线下面试的角度来(提前模拟面试),不断练习就好了,希望这一点对你有所帮助,@路正银 加油 ✗拳头✗

  5. 李鸿翼

    1.volatile使用场景有服务的优雅关闭
    2.volatile主要解决可见性和有序性
    3.volatile底层实现原理是lock前缀指令和内存屏障

    • mikechen

      言简意赅,volatile的底层实现原理还可以从可见性与有序性的角度,画出对应的技术原理图 ,这样会更有助于理解 ✗咧嘴笑✗