理解JMM和Volatile

简介: 理解JMM和Volatile

什么是Volatile?

volitile是Java虚拟机提供轻量级的同步机制,它有以下三个重要概念

  1. 保证可见性(和JMM挂钩)
  2. 不保证原子性
  3. 禁止指令重排

什么是JMM?

JMM:Java内存模型,是一个不存在的东西,它是一个概念,是一个约定

关于JMM的一些同步的约定

1 线程解锁前 必须把共享变量刷回主存

2 线程加锁前 必须读取内存中的最新值到工作内存中

3 必须保证加锁和解锁是同一把锁

线程分为工作内容和主内存

JMM有八种内存交互操作

内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许例外)

    lock   (锁定):作用于主内存的变量,把一个变量标识为线程独占状态

unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定

read  (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用

load   (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中

use   (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令

assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中

store  (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用

write (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中

JMM对这八种指令的使用,制定了如下规则:

不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须     write

 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存

不允许一个线程将没有assign的数据从工作内存同步回主内存

一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是怼变量实施use、store操作之前,必须经过assign和load操作

一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁

如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值

如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量

对一个变量进行unlock操作之前,必须把此变量同步回主内存

代码举例

package com.wyh.volatileDemo;
import java.util.concurrent.TimeUnit;
/**
 * @program: JUC
 * @description:JMMD和Volatile
 * @author: 魏一鹤
 * @createDate: 2022-03-09 23:44
 **/
public class volatileDemo01 {
private static int num=0;
//主线程 main线程
    public static void main(String[] args) throws InterruptedException {
//再开启一条线程
        new Thread(()->{
//一直循环
            while (num==0) {
            }
        }).start();
//休眠1s
        TimeUnit.SECONDS.sleep(1);
//重新赋值num
        num=1;
//打印num
        System.out.println(num); //1
    }
}

发现问题,变量修改但是没有通知程序,导致一直执行,这个时候就需要使用Volatile进行同步了

  1. 使用volatile验证问题
  2. 保证可见性

给变量增加volatile变量修饰

package com.wyh.volatileDemo;
import java.util.concurrent.TimeUnit;
/**
 * @program: JUC
 * @description:JMMD和Volatile
 * @author: 魏一鹤
 * @createDate: 2022-03-09 23:44
 **/
public class volatileDemo01 {
//volatile修饰变量
    //不加volatile程序就会死循环 没有可见性
    private volatile static int num=0;
//主线程 main线程
    public static void main(String[] args) throws InterruptedException {
//再开启一条线程
        new Thread(()->{
//一直循环 /死循环 对主内存的数据变化是不知道的
            //解决办法 给用到的变量使用volatile修饰.增加可见性
            while (num==0) {
            }
        }).start();
//休眠1s
        TimeUnit.SECONDS.sleep(1);
//重新赋值num
        num=1;
//打印num
        System.out.println(num); //1
    }
}

通过结果发现使用volatile修饰变量之后就增加了可见性

  1. 不保证原子性

 原子性:不可分隔 要么全部成功 要么全部失败

线程A在执行任务的时候是不能被打扰的,也不能被分隔,要么同时成功,要么同时失败

     代码测试

package com.wyh.volatileDemo;
import static java.lang.Thread.activeCount;
/**
 * @program: JUC
 * @description: 验证Volatile的不可原子性
 * @author: 魏一鹤
 * @createDate: 2022-03-10 23:31
 **/
//不保证原子性
public class volatileDemo02 {
    //volatile不会保证原子性 输出结果:19571
    private volatile static int num=0;
//只要调用一次方法就让num自增一次
    //synchronized
    public   static void add(){
num++;
    }
public  static void main(String[] args){
//20个线程同时调用add方法  每个线程调用1000次,理论上结果应该=20000
        for (int i = 1; i <= 20; i++) {
new Thread(()->{
for (int i1 = 0; i1 < 1000; i1++) {
add();
                }
            }).start();
        }
//判断线程存活 线程存活才进行操作
        while (activeCount()>2){ //main gc
            //礼让线程
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName()+":"+num); // 没有做原子性操作 结果为19857
        //如果解决这个原子性问题呢
        //1 把add方法使用synchronized关键字进行修饰 保证线程同一执行 结果为20000
        //2 加lock
    }
}

如何不加lock和synchroinzed,怎么样保证原子性?  使用原子类,解决原子性问题

代码如下

package com.wyh.volatileDemo;
import java.util.concurrent.atomic.AtomicInteger;
import static java.lang.Thread.activeCount;
/**
 * @program: JUC
 * @description: 验证Volatile的不可原子性
 * @author: 魏一鹤
 * @createDate: 2022-03-10 23:31
 **/
//不保证原子性
public class volatileDemo02 {
//volatile不会保证原子性 输出结果:19571
    //解决原子性就要用原子类 把int换成AtomicInteger AtomicInteger:原子类int
    private  static AtomicInteger num=new AtomicInteger();
//只要调用一次方法就让num自增一次
    //synchronized
    public   static void add(){
// num++;  //不是一个原子性操作
        num.getAndIncrement(); //= +1 并不是简单的+1 而是用的CAS(CPU的并发原理) 效率极高
    }
public  static void main(String[] args){
//20个线程同时调用add方法  每个线程调用1000次,理论上结果应该=20000
        for (int i = 1; i <= 20; i++) {
new Thread(()->{
for (int i1 = 0; i1 < 1000; i1++) {
add();
                }
            }).start();
        }
//判断线程存活 线程存活才进行操作
        while (activeCount()>2){ //main gc
            //礼让线程
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName()+":"+num); // 没有做原子性操作 结果为19857
        //如果解决这个原子性问题呢
        //1 把add方法使用synchronized关键字进行修饰 保证线程同一执行 结果为20000
        //2 加lock
    }
}

原子类为什么这么高级

属于CAS(CPU并发原理),效率极高,等到CAS知识点再详细说明

这些原子类的底层都直接和操作系统挂钩,直接在内存中修改值

  1. 禁止指令重排

什么是指令重排?

它是一种思想,我们写的程序,计算机并不是按照我们写的那样去执行的

源代码->编译器优化的重排->指令并行也可能重排->内存系统也会重排->执行

处理器在进行指令重排的时候,程序会考虑数据之间的依赖性

如何解决指令重排?

使用volatile可以避免指令重排.增加一个内存屏障(禁止上面指令和下面指令顺序交换,保证特定的操作的执行顺序)

指令重排在哪些场景使用的最多?

单例模式(DCL懒汉式)

目录
相关文章
|
6月前
|
缓存 安全 Java
JMM之可见性介绍
JMM之可见性介绍
44 0
|
3月前
|
缓存 安全 Java
简单了解下JMM解决什么问题
Java内存模型(JMM)是Java语言规范的一部分。JMM通过“happens-before”规则和内存屏障等机制,确保在多线程程序中,各线程对共享变量的操作行为符合预期。
43 3
|
5月前
|
存储 缓存 Java
深入理解JMM
深入理解JMM
121 2
|
6月前
|
缓存 Java 编译器
JMM内存模型 volatile关键字解析
JMM内存模型 volatile关键字解析
46 0
|
缓存 Java 编译器
05.深入理解JMM和Happens-Before
大家好,我是王有志。今天我们一起来学习Java并发编程中最重要的两个理论知识JMM和Happens-Before原则。
121 1
05.深入理解JMM和Happens-Before
|
存储 缓存 Java
到底什么是内存可见性?
到底什么是内存可见性?
147 0
|
存储 缓存 Java
关于JMM的理解
JMM是java内存模型,它描述的是和多线程相关的一组规范。通过这组规范定义了程序中对各个变量的访问方式。保证了不同jvm运行并发程序的结果的一致性和可靠性。
106 0
|
存储 SQL 缓存
|
缓存 Java
内存可见性引发的思考
内存可见性引发的思考
内存可见性引发的思考
volatile与JMM(二)
问:volatile凭什么可以保证可见性和有序性 答: 内存屏障
100 0
volatile与JMM(二)