聊聊Java内存模型

Java内存模型

在这里插入图片描述

1.什么是Java内存模型

在聊Java内存模型时,首先要搞清楚什么是Java内存模型?所谓内存模型是规范或规则,这个规则定义了线程对各种变量的访问方式。这种变量针对的是堆里面的共享变量,例如实例对象,数组等。CPU在摩尔定律下,速度在每18个月就翻一倍,然而内存和硬盘速度远远不及,为了解决这些问题,就诞生了CPU多级缓存架构。但对于这些变量的操作,在Java中所说的每次操作后都会对主内存的数据拷贝到工作内存,形成变量副本。由于工作内存是线程私有的,那么对于一个线程操作共享变量来说,其他线程的操作,该线程如何知道呢?或者说JMM怎么对变量副本控制的呢?

在这里插入图片描述

2.JMM存在的必要性

再谈一谈它的必要性,由于Java内存模型和硬件内存架构并不完全一致,对于硬件内存,有寄存器,缓存L1,L2,L3的概念,对于JMM有着工作内存(线程私有数据区域)和主内存(堆内存)之分。而之前说了JMM只是一种规范或规则,其实在不同的系统架构下,硬件的架构也是不同的,JMM就是屏蔽各种硬件和不同操作系统的内存访问差异,这样才能实现Java程序在各种平台下对内存访问有一致性效果。

在这里插入图片描述

3.三大问题

说了这么多,对于变量的操作,其实就是三大问题:原子性,可见行,有序性。

一.原子性

原子性:指一个操作不可中断,即使在多线程的环境下,一个操作一旦开始就不会被其他线程影响。

在Java中,对于基本数据类型的读取和赋值操作都是原子性操作。但是对于32位系统来说,long和double并非是原子性的,每次读写都是32位,而long和double是64位的存储单元,这样会导致一个线程在写时,操作完前32位的原子操作后,轮到B线程读取时,恰好只读取 到了后32位的数据,这样可能会读取到一个既非原值又不是线程修改值的变量,它可能 是“半个变量”的数值,即64位数据被两个线程分成了两次读取。其实也不用担心,一般用的JVM都是64位的,所以不必在意。(在终端用java -version看一看,有个64-Bit)

在这里插入图片描述

x = 10;   //原子性
y = x;    //变量之间的赋值,不是原子性
x++;      //对变量进行复合操作,不是原子性
x = x + 1 // 同x++一样,不是原子性

代码实操:对于num进行10000次的累加,会发现输出并不是10000.

public class Test {
    public  int num = 0;
    public void increase() {
        num++;
    }

    public static void main(String[] args) {
        Test test = new Test();
        CountDownLatch countDownLatch = new CountDownLatch(1000);
        for (int i = 0; i < 10000; i++){
            new Thread(() -> {
                test.increase();
                countDownLatch.countDown();
            }).start();
        }
        
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(test.num);
    }
}

二.可见性

一个线程的修改另一线程不知

通过上面可知道,线程对共享变量的操作都是线程拷贝到各自的工作内存进行操作后才写回到主内存中的,这就可能存在一个线程A修改了 共享变量x的值,还未写回主内存时,另外一个线程B又对主内存中同一个共享变量x进行操 作,但此时A线程工作内存中共享变量x对线程B来说并不可见,这种工作内存与主内存同步 延迟现象就造成了可见性问题。

假设主内存中存在一个共享变量x,现在有A和B两条线程分别对该变量x=1进行操作, A/B线程各自的工作内存中存在共享变量副本x。假设现在A线程想要修改x的值为2,而B线 程却想要读取x的值,那么B线程读取到的值是A线程更新后的值2还是更新前的值1呢?答案 是,不确定,即B线程有可能读取到A线程更新前的值1,也有可能读取到A线程更新后的值 2,这是因为工作内存是每个线程私有的数据区域,而线程A变量x时,首先是将变量从主内 存拷贝到A线程的工作内存中,然后对变量进行操作,操作完成后再将变量x写回主内,而 对于B线程的也是类似的,这样就有可能造成主内存与工作内存间数据存在一致性问题,假 如A线程修改完后正在将数据写回主内存,而B线程此时正在读取主内存,即将x=1拷贝到 自己的工作内存中,这样B线程读取到的值就是x=1,但如果A线程已将x=2写回主内存后, B线程才开始读取的话,那么此时B线程读取到的就是x=2,但到底是哪种情况先发生呢?
在这里插入图片描述

三.有序性

有序性是指对于单线程的执行代码,我们总是认为代码的执行是按顺序依次执行的,这样的理解并没有毛病,毕竟对于单线程而言确实如此,但对于多线程环境,则可能出现乱序 现象,因为程序编译成机器码指令后可能会出现指令重排现象,重排后的指令与原指令的顺 序未必一致。

在聊有序性之前,先聊聊指令重排,因为没有指令重排也没有有序性可讲。

指令重排:是指编译器和处理器在不影响代码执行结果的前提下,对源代码进行优化排序。

重排序主要分为二种:

  1. 编译器重排序,在不改变单线程程序语义下,重新排序优化
  2. 处理器重排序,将多条指令重叠执行,对于指令的读和写

在这里插入图片描述

其实我的理解,对于编译器重排序,对于下面的2没有依赖于1,而3依赖于1,所以可能重排序如图。

int x,y;
x = 1; //  1
y = 1; //  2
x = x + 1;// 3

在这里插入图片描述

对于指令集的重排序,指令操作主要三点无非是读取,使用,写入主内存。

在这里插入图片描述

要明白的是,在Java程序中,倘若在本线程内,所有操作都视为有序行为,如 果是多线程环境下,一个线程中观察另外一个线程,所有操作都是无序的,前半句指的是单线程内保证串行语义执行的一致性,后半句则指指令重排现象和工作内存与主内存同步延迟现象。

代码实操:会出现

  • 第一种:x = 1,y = 1;

  • 第二种:x = 0, y = 1;

    当x = 0时,说明第一个run运行完,a = 1;第二个run还没有运行,所以造成y = a 运行了,所以 y = 1。

    所以当x = 0,y = 0时一定发生了第一个run中 a = 1和x = b重排序或第二个run重排序了。

import java.util.concurrent.CountDownLatch;
public class Test2 {
    private static int x = 0,y = 0, a = 0, b = 0;
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < Long.MAX_VALUE; i++){
            x = 0; y = 0; a = 0; b = 0;
            CountDownLatch countDownLatch = new CountDownLatch(2);
            new Thread(new Runnable() {
                @Override
                public void run() {
                    a = 1;
                    x = b;
                    countDownLatch.countDown();
                }
            }).start();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    b = 1;
                    y = a;
                    countDownLatch.countDown();
                }
            }).start();
            countDownLatch.await();
            System.out.println(i);
            if (x == 0 && y == 0){
                System.out.println("第" + i + "次进行重排序");
                break;
            }

        }
    }
}

4.疑问

那么对于以上问题,JMM是如何解决的,下回在细细分析【😊】点击下一篇JMM和Volatile

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值