Java并发突击-JMM

155 阅读6分钟

什么是JMM

JMM(Java内存模型Java Memory Model 简称JMM),JMM是一种抽象概念,描述的是一组规则或规范,通过这组规则控制程序中各个变量在共享数据区(主内存)域和私有数据区域(工作内存)的访问方式.

JMM是围绕原子性、有序性、可见性展开的。

JMM与内存的交互

关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步到主内存之间的实现细节,Java内存模型定义了以下八种操作来完成:

jia.png

  • lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。
  • unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  • read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
  • load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
  • use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  • store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
  • write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。

Java内存模型还规定了在执行上述八种基本操作时,必须满足如下规则

  • 如果要把一个变量从主内存中复制到工作内存,就需要按顺寻地执行read和load操作, 如果把变量从工作内存中同步回主内存中,就要按顺序地执行store和write操作。但Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。
  • 不允许read和load、store和write操作之一单独出现
  • 不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中。
  • 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。
  • 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
  • 一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。lock和unlock必须成对出现
  • 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值
  • 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。
  • 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。

JMM内存可见性保证

  • 按程序类型,Java程序的内存可见性保证可以分为下列3类:
    • 单线程程序。单线程程序不会出现内存可见性问题。编译器、runtime和处理器会共同确保单线程程序的执行结果与该程序在顺序一致性模型中的执行结果相同。
    • 正确同步的多线程程序。正确同步的多线程程序的执行将具有顺序一致性(程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同)。这是JMM关注的重点,JMM通过限制编译器和处理器的重排序来为程序员提供内存可见性保证。
    • 未同步/未正确同步的多线程程序。JMM为它们提供了最小安全性保障:线程执行时读取到的值,要么是之前某个线程写入的值,要么是默认值未同步程序在JMM中的执行时,整体上是无序的,其执行结果无法预知。 JMM不保证未同步程序的执行结果与该程序在顺序一致性模型中的执行结果一致。

volatile

  • volatile是Java虚拟机提供的轻量级的同步机制。
    • 可见性,有序性(禁止指令重排) 是它的主要特征。

    对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性(基于这点,我们通过会认为volatile不具备原子性)。volatile仅仅保证对单个volatile变量的读/写具有原子性,而锁的互斥执行的特性可以确保对整个临界区代码的执行具有原子性。

volatile可见性

  • 原理

    volatile修饰的变量的read、load、use操作和assign、store、write必须是连续的,即修改后必须立即同步回主内存,使用时必须从主内存刷新,由此保证volatile变量操作对多线程的可见性。

package com.yqj.juc.jmm;  

/**  
 * volatile可见性  
 *  
 * @author Zhao Yun Long  
 * @version V1.0  
 * @date 2022/10/24 15:06  
 */
 class FlagVar{  
    boolean flag = true;
    //使用volatile
    //volatile boolean flag = true;  
    public void changeFlag(){  
        flag = false;  
        System.out.println(Thread.currentThread().getName() + "修改flag:" + flag);  
    }  
}  
  
public class VisibilityDemo {  
  
    public static void main(String[] args) {  
        FlagVar flagVar = new FlagVar();  
        // 开启一个线程修改flag的值  
        new Thread(() ->{  
            try {  
                Thread.sleep(1000);  
            } catch (InterruptedException e) {  
                throw new RuntimeException(e);  
            }  
            System.out.println(Thread.currentThread().getName() + "开始执行");  
            flagVar.changeFlag();  
        },"AAA").start();  
        //主线程判断flag的值,为true时一直死循环  
        while (flagVar.flag){  
            //  
        }  
        //flag为false时执行如下代码  
        System.out.println(Thread.currentThread().getName() + "执行完成!!!");  
    }  
  
}

不使用volatile时:

JMM.gif

使用volatile时:

JMM_VOLATILE.gif

volatile不保证原子性

我们上边提到:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性,下边我们进行代码演示,具体看下.

package com.yqj.juc.jmm;  
/**  
 * volatile不保证原子性  
 * 将num使用10个线程进行++  
 * @author Zhao Yun Long  
 * @version V1.0  
 * @date 2022/10/24 15:06  
 */
 class NumData{  
    volatile int num = 0;  
    public void numAdd(){  
        num++;  
    }  
}  
public class AtomDemo {  
    public static void main(String[] args) {  
        while (true){  
            NumData numData = new NumData();  
            //预期值最终num=1000  
            for (int i = 0; i < 1000; i++) {  
                new Thread(() ->{  
                    numData.numAdd();  
                },"Thread"+ i).start();  
            }  
            System.out.println(Thread.currentThread().getName()+"num最终值:"+ numData.num);  
        }  
    }  
}

JMMvo_atom.gif

votatile禁止指令重排

image.png

  • volatile禁止重排序场景:
    1. 第二个操作是volatile写,不管第一个操作是什么都不会重排序
    2. 第一个操作是volatile读,不管第二个操作是什么都不会重排序
    3. 第一个操作是volatile写,第二个操作是volatile读,也不会发生重排序