Java并发编程之内存模型

计算机内存模型

CPU的缓存结构

计算机在执行程序时,每条指令都是在CPU中执行的,而指令在执行的过程中涉及到的数据,则是存放在主内存(物理内存)当中。

当CPU直接和主内存进行通信,我们执行一条指令的过程是:从内存中读出需要进行计算的数据>>>进行计算>>>计算完成>>>把计算后得到的数据写入内存。
在这里插入图片描述
随着CPU性能的提升,CPU计算速度越来越快,内存数据的读取和写入速度成为了制约计算机性能的主要因素。

为了解决这个问题,人们想出来了一个好的办法,就是在CPU和内存之间增加高速缓存(高速缓存本质也是一块内存,它的特点是速度快,内存小,并且昂贵)。

当加入高速缓存之后,CPU执行指令的过程就变为了:高速缓存从内存中copy数据>>>CPU从高速缓存中读取数据>>>进行计算>>>计算完成>>>CPU将结果写入高速缓存>>>高速缓存将数据update到内存。
在这里插入图片描述
随着CPU能力的不断提升,一层缓存就慢慢的无法满足要求了,就逐渐的衍生出多级缓存。

按照数据读取顺序和与CPU结合的紧密程度,CPU缓存可以分为一级缓存,二级缓存,部分高端CPU还具有三级缓存,每一级缓存中所储存的全部数据都是下一级缓存的一部分,这三种缓存的技术难度和制造成本是相对递减的,所以其容量也是相对递增的。

那么,在有了多级缓存之后,程序的执行就变成了:当CPU要读取一个数据时,首先从一级缓存中查找,如果没有找到再从二级缓存中查找,如果还是没有就从三级缓存或内存中查找。

当CPU的能力继续提升,由单核CPU变为多核CPU,这时CPU的每个核心都会有一套自己的一级缓存、二级缓存,同时这些核心最终的数据还是得同步到共享内存(三级缓存、主内存)当中。此时CPU的缓存结构就变为了下图:
在这里插入图片描述

缓存不一致问题

当在CPU和主存间加上了缓存,CPU的每个核有了自己的缓存,同一个数据在不同核中的缓存内容就有可能不一致。在多线程的场景下,就会出现缓存不一致性的问题。

我们分别来分析下单线程、多线程在单核CPU、多核CPU中的影响。

单线程: 缓存只被一个线程操作,不存在读写冲突,无缓存不一致的问题。

单核多线程: 单核多线程的情况下,单个cpu时间分片内只会有一个线程对同一个缓存空间进行操作,也不会存在读写冲突,无缓存不一致的问题。

多核多线程: 多核多线程的情况下,每个核心的私有缓存(L1、L2)的内容并不会实时的同步到共享内存当中,当多个线程在相同的cpu时间分片,通过不同的核心对同一个数据进行操作,就会出现缓存不一致的问题。

处理器优化和指令重排

上面提到CPU和主存之间增加缓存,在多线程场景下会存在缓存一致性问题。除了这种情况,还有一种硬件问题也比较重要。那就是为了使处理器内部的运算单元能够尽量的被充分利用,处理器可能会对输入代码进行乱序执行处理。这就是处理器优化。(在Java中JVM的即时编译器(JIT)就会通过指令重排来对代码进行优化)

同样的,在多核多线程的情况下,当多个线程在相同的cpu时间分片通过不同的核心执行相同的程序代码时,如果任由处理器优化和编译器对指令重排的话,就可能导致各种各样的问题。

并发编程面对的问题

通过上述分析,在多线程的场景下,会存在缓存不一致&指令重排导致的问题。因此在进行并发编程的时候,我们就得考虑并且解决这些问题。

并发编程,为了保证数据的安全,需要满足以下三个特性:

原子性原子性是指一个操作是不可中断的,要么全部执行成功要么全部执行失败,即在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰。

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

有序性即程序执行的顺序按照代码的先后顺序执行。

什么是内存模型

为了保证共享内存的正确性(可见性、有序性、原子性),内存模型定义了共享内存系统中多线程程序读写操作行为的规范。 通过这些规则来规范对内存的读写操作,从而保证指令执行的正确性。它与处理器有关、与缓存有关、与并发有关、与编译器也有关。他解决了CPU多级缓存、处理器优化、指令重排等导致的内存访问问题,保证了并发场景下的一致性、原子性和有序性。

内存模型解决并发问题主要采用两种方式:限制处理器优化使用内存屏障。

Java内存模型

通过前面计算机内存模型的介绍,我们知道内存模型是解决多线程场景下并发问题的一个重要规范。

那么Java内存模型(Java Memory Model ,JMM) 就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。

Java的内存结构

Java内存模型规定了所有的变量数据都存储在主内存中,每条线程还有自己的工作内存。线程的工作内存中保存了该线程中用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。不同的线程之间的工作内存是互相隔离的,不允许直接交叉访问,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。

Java内存模型中的内存结构,是一种内存的逻辑划分,它和Java程序运行基本原理中java运行时的数据区是两个不相同的东西。
在这里插入图片描述

Java内存模型的实现

Java中提供了一系列和并发处理相关的关键字,比如volatilesynchronizedfinalconcurrent包等。这些就是Java内存模型封装了底层的实现后提供给程序员使用的工具。

在开发多线程的代码的时候,我们可以直接使用synchronized等关键字来控制并发,从来就不需要关心底层的编译器优化、缓存一致性等问题。所以,Java内存模型,除了定义了一套规范,还提供了一系列原语,封装了底层实现后,供开发者直接使用。

可见性

通过内存模型,我们可以知道,线程的工作内存维护了一份自己的缓存数据,线程直接修改的是工作内存中的缓存。
假设线程A对某个变量m进行了修改,由于线程间的工作内存是隔离的,此时线程B是不能直接读到这个修改后的m的,只有等线程A将m同步到主内存,然后线程B从主内存中读取了更改后的m再copy到自己的工作内存当中,线程B才能获取到修改后的m。在这之前,线程A修改的变量m对线程B是不可见的。

在Java中,volatile关键字提供了一个功能,那就是被其修饰的变量在被修改后可以立即同步到主内存,被其修饰的变量在每次是用之前都从主内存刷新。因此,可以使用volatile来保证多线程操作时变量的可见性。

除了volatile,Java中的synchronized和final两个关键字也可以实现可见性。

原子性

在Java中可以使用关键字synchronized来保证方法和代码块内的操作的原子性,通过javap解析的.class文件可以看到,它对应的字节码指令为monitorenter和monitorexit。

同时Java还有原语层级的CAS操作也可以保证原子性,concurrent包里面的Automic类就是通过CAS操作来实现操作的原子性的。

有序性

在Java中,synchronized和volatile都可以用来保证多线程之间操作的有序性,volatile关键字会禁止指令重排。synchronized关键字保证同一时刻只允许一条线程操作。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值