并发编程之JMM模型

前言

JMM内存模型是从cpu调度的最小单位线程出发,包含了工作内存和主内存的设计理念,并围绕着原子性,可见性,有序性进行展开,其中无论你的服务器是单核还是多核,在多线程的情况下都会存在可见性的问题。

JMM内存模型是以多线程为出发背景,而缓存一致性是以多核为出发背景,虽然缓存也是属于cpu的一部分,并在线程里有所体现,并且缓存实际上会存储(拷贝)了主存的堆对象。但是在写这篇文章时,为了避免造成太多困扰(笔者曾被这两个概念交叉困扰不已),我暂且不去考虑缓存。 高速缓存的文章请参考我另外一篇文章Java缓存–缓存一致性,这两篇结合起来看可能会更加明朗一些。

什么是JMM模型

在这里插入图片描述
从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。

jmm将java内存划分为共享的主内存(可以简单理解为堆内存)以及各个线程之间隔离的栈内存(可以简单认为是寄存器,高速缓存以及缓存和指令集的处理集合)。

用户内存和主存到底存了什么?

cpu的最小调度单位为线程,cpu会为每个线程创建工作内存,线程又由一流程的栈帧组合,栈帧中会维护各自的局部变量以及操作数栈等,局部变量如果是对象的话,其引用地址也会指向到堆内存空间(主存)。这些局部变量是由线程独享的,也可以理解为工作内存中会存放局部变量的引用地址。

而主存中包含了方法区(类元信息,静态变量,常量池等)和堆(对象实例等)。

但是栈帧中对应的方法里不可能只包含局部变量,也会包含全局变量的调用,所以jvm也会将全局变量拷贝到用户内存中(注意这里拷贝的只是对象的引用地址,并非对象数据)。

不过有一点要注意,线程是需要执行代码的,而执行代码肯定会有操作数栈,操作数栈是一个用于计算的临时数据存储区,那不管是全局变量还是局部变量的值,最后都会通过弹栈与压栈的方式进行读取和返回结果。所以工作内存虽然只拷贝了全局共享变量,但是在计算的时候会通过引用地址寻址到对应的内存数据(可能是在缓存中也可能是在主存中),并会将涉及到计算部分的数据读取到操作数栈里去,如果全局变量在线程中涉及到了修改,也会回写到主存里去。

并发编程的原子性,可见性,有序性问题。

原子性

一个操作或多个操作要么全部执行完成且执行过程不被中断,要么就不执行。

在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断,要么执行,要么不执行。

X=10; //原子性(简单的读取、将数字赋值给变量)

Y = x; //变量之间的相互赋值,不是原子操作

X++; //对变量进行计算操作

X = x+1;

语句2实际包括两个操作,它先要去读取x的值,再将y值写入,两个操作分开是原子性的。合在一起就不是原子性的。

语句3、4:x++ x=x+1包括3个操作:读取x的值,x+1,将x写入
重点:只要一个操作要分多步来执行,基本就违背了原子性。(因为并发情况下,可以影像到其中一个环节的全局变量状态)

解决方案:可以通过 synchronized和Lock实现原子性。因为synchronized和Lock能够保证任一时刻只有一个线程访问该代码块。

可见性

多个线程同时访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

比如有一个全局变量count,线程a读取了count=0,并执行了count++操作,且刷新了主存(count=1);此时有个线程b也读取了count(count=0),是在线程a执行count++之前,并且线程b又执行了一些其他操作后,此时线程a已经执行了count++且刷入主存(此时count=1),但是线程b再执行count++操作时,count还是为0。这样问题就大了,你可以理解成线程b无法感知到主存的刷新。

解决方案:Java提供了volatile关键字保证可见性。当一个共享变量被volatile修饰时,它会保证修改的值立即被其他的线程看到,即修改的值立即更新到主存中,当其他线程需要读取时,它会去内存中读取新值。

当然你也可以通过锁来实现,因为锁保证了同一个时刻只有一个线程访问代码块,那下一个线程访问代码块的前提一定是上一个线程已经把需要修改的全局变量数据回写到了主存。

有序性

java程序在编译为机器代码后,可能会发生指令重排,即只要程序的最终结果 与它顺序化情况的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的重排序。而之所以需要指令重排,应该是处于jvm自身的优化考虑。
我们以个单例的代码做为例子:

public class InstuctionReorder {
    // new Object()分为三步:
    // 1)分配对象内存空间
    // 2)初始化对象
    // 3)为instance变量设置刚分配的内存地址
    // 步骤2,3是不存在数据依赖的,那可能会指令重排;
    // 如果一个线程最先抢到了锁,并执行了new Object(),但是它且只执行了1,3两步
    // 这时候又进来一个线程,会发现instance不为null直接返回了,那这个instance其实是还没有执行第二步的初始化对象的。
    private static  Object instance;

    public static Object getInstance() {
        if (instance == null) {
            synchronized (InstuctionReorder.class) {
                //为什么要进行双重判断呢?
                //高并发下,可能导致多个线程同时进来抢锁且阻塞,则会导致instance被初始化多次
                if (instance == null) {
                    instance = new Object();
                }
            }
        }
        return instance;
    }
}

在这里插入图片描述
所以我们这种情况下,只需要对instance变量加上volatile的修饰来禁止指令重排。

    private static volatile  Object instance;

指令重排的禁止是通过硬件层的内存屏障来实现的,它会对各种情况下读写操作的是否可以重排来进行限制。

总结

JMM内存模型是对于多线程并发下引导出来的一种抽象的设计概念,它是围绕这原子性,可见性,有序性进行展开。

synchronized,lock可以解决原子性,可见性,有序性的所有问题。

valotile可以解决可见性和有序性的问题,但是解决不了原子性。

哪怕是单核情况下,也会有可见性问题(只要是高并发的情况),但是不会有缓存一致性问题,这里一定不要搞混淆。

如果valotile修饰的是对象,比如是一个数组array,那array[i]也是具备可见性的。

不管是不是高并发情况下,类实例、对象肯定还是存放在堆或缓存中,类元信息、常量池、静态变量肯定也是存放在方法区;只不过用户内存会从主存里拷贝全局共享变量(可以理解为引用地址),程序执行时cpu会通过地址寄存器保存的地址进行寻址,并将对象里涉及到的数据(基础类型)存放到操作数栈进行计算。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值