JMM简单学习

    技术2022-07-17  79

    JMM学习

    学习资料:

    b站诸葛老师-JMM篇

    一、系统CPU和主内存交互图:

    二、JMM模型

    Java线程内存模型跟cpu缓存模型类似,是基于cpu缓存模型来建立的,java线程内存模型是标准化的,屏蔽掉了底层不同计算机的区别

    注意:每个线程操作共享变量操作的是复制主内存的副本,当一个线程修改了自己工作内存中变量,对其他线程是不可见的,会导致线程不安全的问题。下面代码演示这种问题:

    代码测试:

    package com.lxf.volatileT; import java.util.concurrent.TimeUnit; /** * 两个线程访问同一个共享变量,一个线程修改完共享变量后,另一个线程对这个共享变量是不可见的 */ public class JMMDemo { private static boolean flag=true; public static void main(String[] args) { new Thread(()->{//线程1 while (flag){ } }).start(); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } flag=false; System.out.println("flag="+flag); } }

    结果:打印flag=true,但是线程1并未停止

    解决方法:

    package com.lxf.volatileT; import java.util.concurrent.TimeUnit; /** * volatile实现实现之间共享变量可见 *volatile作用:线程之间可见、有序性(防止指令重排)、不能保证原子性 */ public class JMMDemo { private static volatile boolean flag=true; public static void main(String[] args) { new Thread(()->{//线程1 while (flag){ } }).start(); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } flag=false; System.out.println("flag="+flag); } }

    在打印flag=true之后,线程1也停止了

    三、JMM数据原子操作、volatile原理、volatile特性

    3.1、JMM数据原子操作
    read(读取):从主内存读取数据load(载入):将主内存读取到的数据写入工作内存use(使用):从工作内存读取到的数据来计算assign(赋值):将计算好的值重新赋值到工作内存中store(存储):将工作内存数据写入主内存write(写入):将sotre过去的变量值赋值给主内存中的变量lock(锁定):将主内存变量加锁,标识为线程独占状态unlock(解锁):将主内存变量解锁,解锁后其它线程可以锁定该变量
    3.2、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操作之前,必须把此变量同步回主内存

    3.3、查看底层汇编指令
    下载显示汇编代码的插件放入jdk中java程序汇编代码查看:-server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:CompileCommand=compileonly,*VolatileVisbilityTest.prepareData
    3.4、根据汇编代码探究Volatile缓存可见性实现原理
    底层实现主要是通过汇编lock前缀指令,它会锁定这块内存区域的缓存(缓存行锁定)并回写到主内存。IA-32架构软件开发者手册对lock指令的解释: 会将当前处理器缓存行的数据立即写回到系统内存。这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效(MESI协议)

    3.5、volatile的特性
    首先我们要知道并发编程的三大特性:可见性、原子性、有序性Volatile保证可见性与有序性,但是不保证原子性,保证原子性需要借助synchronized、lock这样的锁机制

    Volatile不保证原子性代码测试:

    package com.lxf.jmm; public class VolatileAtomicTest{ public static volatile int num=0; public static void increase(){ num++; } public static void main(String[] args) throws InterruptedException { Thread[] threads=new Thread[10]; for (int i = 0; i < threads.length; i++) { threads[i]=new Thread(()->{ for (int j = 0; j < 1000; j++) { increase(); } }); threads[i].start(); } //等所有线程执行完了再执行main for (Thread thread : threads) { thread.join(); } System.out.println("num="+num); } }

    结果是<=10000,多次运行就能看到。

    分析原因:

    num++是一组操作(假设num值初始值为0),里面分为三步原子操作:读取到num值,num值加1,将num值写回缓存,这三步操作都有可能阻塞。假如一个线程读取num值后阻塞,然后另一个线程执行完了:将num赋值为1,然后这个线程继续执行将值仍然返回为1,这就造成了上面的问题。

    参考博文:volatile为什么不能保证原子性

    Volatile保证有序性测试(禁止指令重排)

    package com.lxf.jmm; import java.util.HashMap; import java.util.Map; public class VolatileSerialTest { static int x=0,y=0; public static void main(String[] args) throws InterruptedException { //存结果的Map集合 Map<String,Integer> resultMap=new HashMap<>(); for (int i = 0; i < 1000000; i++) { x=0;y=0; resultMap.clear(); //第一个线程 Thread one=new Thread(()->{ int a=y; x=1; resultMap.put("a",a); }); //第二个线程 Thread two=new Thread(()->{ int b=x; y=1; resultMap.put("b",b); }); //第一个线程开启 one.start(); //第二个线程开启 two.start(); //第一个线程插队 one.join(); //第二个线程插队 two.join(); if(resultMap.get("a")==0&&resultMap.get("b")==0){ System.out.println("===============a等于0,b也等于0================================"); System.out.println("a=" + resultMap.get("a") + ",b=" + resultMap.get("b")); System.out.println("===============a等于0,b也等于0================================"); }else if(resultMap.get("a")==1&&resultMap.get("b")==1){ System.out.println("===============a等于1,b也等于1================================"); System.out.println("a=" + resultMap.get("a") + ",b=" + resultMap.get("b")); System.out.println("===============a等于1,b也等于1================================"); }else{ System.out.println("a=" + resultMap.get("a") + ",b=" + resultMap.get("b")); } } } }

    结果可能性:

    a=0,b=0

    a=0,b=1

    a=1,b=0

    a=1,b=1

    结果分析:前三个可能性我们都知道,当one线程在前结果就是a=1,b=0,当two线程在前结果就是a=0,b=1,当两个线程同时运行:a=0,b=0。但是a=1,b=1这种可能性还是难以理解,但还是存在的(很少见,得大量运行)见下图:

    因为发生了指令重排序:

    解决方法:定义x,y的时候加上volatile关键字就可以解决了

    具体解释看以下博文:

    参考博文: 1. 并发关键字volatile(重排序和内存屏障)

    2.Volatile禁止指令重排

    Processed: 0.008, SQL: 9