JMM简单学习

JMM学习

学习资料:

b站诸葛老师-JMM篇

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LFfRxagn-1593677912974)(E:\typoraPic\image-20200701200952976.png)]

二、JMM模型

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cUlphejS-1593677912982)(E:\typoraPic\image-20200701203102180.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-stMJehgC-1593677912989)(E:\typoraPic\image-20200701213140382.png)]

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

代码测试:

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数据原子操作
  1. read(读取):从主内存读取数据
  2. load(载入):将主内存读取到的数据写入工作内存
  3. use(使用):从工作内存读取到的数据来计算
  4. assign(赋值):将计算好的值重新赋值到工作内存中
  5. store(存储):将工作内存数据写入主内存
  6. write(写入):将sotre过去的变量值赋值给主内存中的变量
  7. lock(锁定):将主内存变量加锁,标识为线程独占状态
  8. unlock(解锁):将主内存变量解锁,解锁后其它线程可以锁定该变量
3.2、JMM对这八种指令的使用,制定了如下规则:
  1. 不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write

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

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

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

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

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

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

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-864yzvcV-1593677912998)(E:\typoraPic\image-20200702101909076.png)]

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这种可能性还是难以理解,但还是存在的(很少见,得大量运行)见下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0WgU29NB-1593677913007)(E:\typoraPic\image-20200702155842771.png)]

因为发生了指令重排序:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2IKY8EZy-1593677913011)(E:\typoraPic\image-20200702160659736.png)]

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

具体解释看以下博文:

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

2.Volatile禁止指令重排

<think>嗯,用户让我用通俗的话解释一下JMM,也就是Java内存模型。首先,我需要确认自己是否真的理解JMM的核心概念。记得JMM主要是为了解决多线程环境下内存访问的一致性问题,对吧?那Java内存模型定义了线程如何与主内存以及工作内存交互,确保在不同硬件和操作系统上都能有确定性的行为。 用户可能是一个刚开始学习Java多线程的开发者,或者对并发编程有些困惑。他们可能遇到了线程安全的问题,比如变量可见性或者指令重排序,这时候JMM就显得很重要了。用户需要的是一个简单易懂的解释,而不是学术性的术语堆砌。 接下来,我需要用类比的方式来解释。比如,主内存就像是公司的共享文档,每个线程(员工)有自己的工作内存(个人笔记)。当员工需要修改文档时,他们得先复制到自己的笔记里,改完后再同步回去。但这样可能导致问题,比如两个人同时修改同一个地方,结果覆盖了对方的更改,或者一个人改了但其他人没及时看到。 然后要提到JMM的关键点:原子性、可见性、有序性。原子性可能用银行转账的例子,但用户可能更熟悉的是像计数器这样的简单操作,比如i++其实不是原子的。可见性方面,可以用一个线程修改了变量,另一个线程却看不到,导致循环无法退出的例子。有序性的话,指令重排序可能会让程序执行顺序和代码顺序不一致,导致意外结果。 还要注意用户可能不知道volatile和synchronized的作用,需要简单说明这些关键字如何帮助解决可见性和有序性问题。比如volatile确保变量的修改立即同步到主内存,而synchronized则通过锁机制保证原子性和可见性。 可能用户在实际编码中遇到了类似问题,比如某个变量在多线程环境下表现异常,这时候理解JMM能帮助他们找到原因。所以需要强调JMM的重要性,以及如何利用这些规则编写线程安全的代码。 最后检查是否有遗漏点,确保覆盖了JMM的三个核心概念,并且例子足够生活化,避免技术术语过多。确保解释清晰,让即使没有太多并发经验的开发者也能理解。J</think>MM(Java Memory Model,Java内存模型)是Java为了在多线程环境下保证程序执行的**正确性**和**可预测性**而设计的一套规则。你可以把它想象成一个“交通规则”,告诉线程什么时候该“停车”、什么时候能“加速”、如何避免“撞车”。 --- ### **用现实例子通俗理解JMM** 假设你和几个同事一起编辑一份共享的在线文档(类比多线程操作共享数据): 1. **问题1:各自电脑的缓存不同步** 每个人(线程)的电脑(工作内存)里都缓存了这份文档的副本。你修改了文档,但同事可能看不到最新内容,直到你点击“保存”(同步到主内存)。 → 这就是**可见性问题**:一个线程修改了数据,其他线程可能无法立刻看到。 2. **问题2:指令顺序被打乱** 老板让你先改标题再改正文,但实际执行时可能先改正文再改标题(为了优化效率)。 → 这就是**有序性问题**:代码的编写顺序和实际执行顺序可能不一致。 3. **问题3:操作被中途打断** 你正在转账,刚扣完钱,同事却插队查余额,看到钱还没到账。 → 这就是**原子性问题**:一个操作被中途打断,导致数据不一致。 --- ### **JMM的核心规则** 1. **可见性(Visibility)** - 规则:线程修改共享变量后,必须立即同步到主内存;其他线程用这个变量时,必须从主内存重新加载。 - 实现方式:用 `volatile` 关键字、`synchronized` 锁、`final` 不可变变量。 2. **有序性(Ordering)** - 规则:代码可能被编译器或CPU优化重排,但要保证“最终结果”和代码顺序一致。 - 实现方式:`volatile` 禁止指令重排、`synchronized` 锁住临界区。 3. **原子性(Atomicity)** - 规则:某些操作必须“一气呵成”,比如转账(扣钱和加钱必须一起完成)。 - 实现方式:用 `synchronized` 锁、原子类(如 `AtomicInteger`)。 --- ### **举个实际代码例子** ```java // 问题代码:两个线程同时执行 i++,结果可能小于预期 public class Test { private int i = 0; public void add() { i++; } // i++ 不是原子操作! } // 用JMM规则修复 public class FixedTest { private volatile int i = 0; // 保证可见性 public synchronized void add() { i++; } // 保证原子性+有序性 // 或者用原子类:private AtomicInteger i = new AtomicInteger(0); } ``` --- ### **总结** - JMM的核心是解决多线程的**可见性、有序性、原子性**问题。 - 开发者需要理解这些规则,用 `volatile`、`synchronized`、原子类等工具写出线程安全的代码。 - 类比:就像多人合作时,必须约定好“什么时候保存文档”“谁先操作”才能不冲突。 理解JMM,就能避免多线程中各种“诡异”的Bug! 🚀
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值