【JVM】谈谈你对volatile的理解

简介: 【JVM】谈谈你对volatile的理解

1. JMM(Java内存模型

1.1 定义及规定

1.1.1 定义

JMM 本身是一种抽象的概念并不是真实存在,它描述的是一组规定或则规范,通过这组规范定义了程序中的访问方式。

1.1.2 规定

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

1.2 三大特性

1.2.1 可见性

线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存

一旦主内存中的变量发生改变,必须让所有的线程都能看见,第一时间通知,这就是可见性

1.2.2 原子性

1.2.3 有序性

2. volatile

1.1 volatile是什么?

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

1.2 三大特性

他的特点和我们的JMM也有点类似,volatile 不保证原子性,保证可见性和禁止指令重排

1.2.1 保证可见性

线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存

如果不加 volatile 关键字,则主线程会进入死循环,加 volatile 则主线程能够退出,说明加了 volatile 关键字变量,当有一个线程修改了值,会马上被另一个线程感知到,当前值作废,从新从主内存中获取值。对其他线程可见,这就叫可见性。

class Mydata {
  volatile int number = 0;
  public void add() {
    this.number = 60;
  }
}
/*
 * 1 验证volatile的可见性 
 *  1.1 假如 int number = 0;
 *      number变量之前根本没有添加volatile关键字修饰
 */
public class SeeOkValatile {
  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 (Exception e) {
        e.getStackTrace();
      }
      mydata.add();
      System.out.println(Thread.currentThread().getName() + "\t update number " + mydata.number);
    }, "aaa").start();
    // 傻乎乎的在这转着
    while (mydata.number == 0) {
    }
    System.out.println(Thread.currentThread().getName() + " ");
  }
}

1.2.2 不保证原子性

1.2.2.1 什么是原子性?

不可分割,完整性,也就是某个线程正在做某个具体业务时,中间不可以被加塞或者被分割,需要整体完整,要么同时成功,要么同时失败

class MydataDemo {
  volatile int number = 0;
  public void add() {
    this.number = 60;
  }
  // 请注意:此时number前面是加了volatile修饰的
  public void addPlus() {
    number++;
  }
  AtomicInteger atomicInteger = new AtomicInteger();
  public void addAtomic() {
    atomicInteger.getAndIncrement();
  }
}
/*
 * 验证volatile的原子性
 * 
 * 2.3 why 对于number++这个操作 分为3步: 
 *      1. A = number 线程从主内存中拿到该值 
 *      2. B = A + 1  在自己的线程内存中加一 
 *      3. number = B 最后存入到主内存中
 *    由于多线程的并发性,可能导致值的覆盖
 * 
 * 2.4 怎么解决?
 *    加synchronizedvoid
 *    使用AtomicInteger,原子类的数据
 */
public class SeeOkValatile2 {
  public static void main(String[] args) {
    MydataDemo mydataDemo = new MydataDemo();
    for (int i = 0; i < 20; i++) {
      new Thread(() -> {
        for (int j = 0; j < 1000; j++) {
          mydataDemo.addPlus();
          mydataDemo.addAtomic();
        }
      }, String.valueOf(i)).start();
    }
    while (Thread.activeCount() > 2) {
      // 礼让线程
      Thread.yield();
    }
    System.out.println(Thread.currentThread().getName() + "number is->" + mydataDemo.number);
    System.out.println(Thread.currentThread().getName() + "number is->" + mydataDemo.atomicInteger);
  }
}

关于count++的知识:

1.2.3 禁止指令重排

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

  • 编译器优化的重排
  • 指令并行的重排
  • 内存系统的重排

  1. 单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。
  2. 处理器在进行重排序时必须要考虑指令之间的数据依颍悝
  3. 多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测
class ReOrderDemo {
    int a = 0;
    boolean flag = false;
    public void write() {
        a = 1;                   //1
        flag = true;             //2
    }
    public void read() {
        if (flag) {                //3
            int i =  a * a;        //4
        }
    }
}
// 单线程:1234
// 多线程:会出现混乱的错误,指令重排
1.2.3.1 禁止指令重排的实现

volatile 实现禁止指令重排序的优化,从而避免了多线程环境下程序出现乱序的现象

内存屏障(Memory Barrier)又称内存栅栏,是一个 CPU 指令,他的作用有两个:

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

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

1.3 线程安全性获得保证

工作内存与主内存同步延迟现象导致可见性问题

  • 可以使用 synchronzied 或 volatile 关键字解决,它们可以使用一个线程修改后的变量立即对其他线程可见

对于指令重排导致可见性问题和有序性问题

  • 可以利用 volatile 关键字解决,因为 volatile 的另一个作用就是禁止指令重排序优化

1.4 你在哪里用到过volatile

单例模式下的DCL

正常情况

  1. 分配对象内存空间
  2. 初始化对象
  3. 设置instance指向刚分配的内存地址,此时instance != null

指令重排

  1. 分配对象内存空间
  2. 设置instance指向刚分配的内存地址,此时instance != null 我们的对象还没有初始化
  3. 初始化对象
public class SingletonDemo {
  private static volatile SingletonDemo instance = null;
  private SingletonDemo() {
    System.out.println(Thread.currentThread().getName() + "我是构造方法");
  }
  // DCL Double Check Lock双端检锁机制
  public static synchronized SingletonDemo getInstance() {
    if (instance == null) {
      synchronized (SingletonDemo.class) {
        if (instance == null) {
          instance = new SingletonDemo();
        }
      }
    }
    return instance;
  }
  public static void main(String[] args) {
    // 单线程(main线程的操作动作----)
    // System.out.println(SingletonDemo.getInstance() ==
    // SingletonDemo.getInstance());
    // System.out.println(SingletonDemo.getInstance() ==
    // SingletonDemo.getInstance());
    // System.out.println(SingletonDemo.getInstance() ==
    // SingletonDemo.getInstance());
    for (int i = 0; i <= 10; i++) {
      new Thread(() -> {
        SingletonDemo.getInstance();
      }, String.valueOf(i)).start();
    }
  }
}


相关文章
|
缓存 安全 算法
Java面试题:如何通过JVM参数调整GC行为以优化应用性能?如何使用synchronized和volatile关键字解决并发问题?如何使用ConcurrentHashMap实现线程安全的缓存?
Java面试题:如何通过JVM参数调整GC行为以优化应用性能?如何使用synchronized和volatile关键字解决并发问题?如何使用ConcurrentHashMap实现线程安全的缓存?
147 0
|
11月前
|
SQL 缓存 Java
JVM知识体系学习三:class文件初始化过程、硬件层数据一致性(硬件层)、缓存行、指令乱序执行问题、如何保证不乱序(volatile等)
这篇文章详细介绍了JVM中类文件的初始化过程、硬件层面的数据一致性问题、缓存行和伪共享、指令乱序执行问题,以及如何通过`volatile`关键字和`synchronized`关键字来保证数据的有序性和可见性。
135 3
|
存储 缓存 安全
Java面试题:介绍一下jvm中的内存模型?说明volatile关键字的作用,以及它如何保证可见性和有序性。
Java面试题:介绍一下jvm中的内存模型?说明volatile关键字的作用,以及它如何保证可见性和有序性。
102 0
|
算法 安全 Java
Java面试题:解释JVM中的堆内存分代收集策略,并讨论年轻代和老年代的特点,描述Java中的线程池,并解释线程池的优点,解释Java中的`volatile`关键字的作用和使用场景
Java面试题:解释JVM中的堆内存分代收集策略,并讨论年轻代和老年代的特点,描述Java中的线程池,并解释线程池的优点,解释Java中的`volatile`关键字的作用和使用场景
110 0
|
4月前
|
Arthas 存储 算法
深入理解JVM,包含字节码文件,内存结构,垃圾回收,类的声明周期,类加载器
JVM全称是Java Virtual Machine-Java虚拟机JVM作用:本质上是一个运行在计算机上的程序,职责是运行Java字节码文件,编译为机器码交由计算机运行类的生命周期概述:类的生命周期描述了一个类加载,使用,卸载的整个过类的生命周期阶段:类的声明周期主要分为五个阶段:加载->连接->初始化->使用->卸载,其中连接中分为三个小阶段验证->准备->解析类加载器的定义:JVM提供类加载器给Java程序去获取类和接口字节码数据类加载器的作用:类加载器接受字节码文件。
402 55
|
5月前
|
Arthas 监控 Java
Arthas memory(查看 JVM 内存信息)
Arthas memory(查看 JVM 内存信息)
360 6
|
8月前
|
存储 设计模式 监控
快速定位并优化CPU 与 JVM 内存性能瓶颈
本文介绍了 Java 应用常见的 CPU & JVM 内存热点原因及优化思路。
879 166
|
10月前
|
缓存 Prometheus 监控
Elasticsearch集群JVM调优设置合适的堆内存大小
Elasticsearch集群JVM调优设置合适的堆内存大小
1649 1
|
6月前
|
存储 缓存 算法
JVM简介—1.Java内存区域
本文详细介绍了Java虚拟机运行时数据区的各个方面,包括其定义、类型(如程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区和直接内存)及其作用。文中还探讨了各版本内存区域的变化、直接内存的使用、从线程角度分析Java内存区域、堆与栈的区别、对象创建步骤、对象内存布局及访问定位,并通过实例说明了常见内存溢出问题的原因和表现形式。这些内容帮助开发者深入理解Java内存管理机制,优化应用程序性能并解决潜在的内存问题。
297 29
JVM简介—1.Java内存区域
|
6月前
|
缓存 监控 算法
JVM简介—2.垃圾回收器和内存分配策略
本文介绍了Java垃圾回收机制的多个方面,包括垃圾回收概述、对象存活判断、引用类型介绍、垃圾收集算法、垃圾收集器设计、具体垃圾回收器详情、Stop The World现象、内存分配与回收策略、新生代配置演示、内存泄漏和溢出问题以及JDK提供的相关工具。
JVM简介—2.垃圾回收器和内存分配策略