java并发基础(二)

一、CPU多级缓存:

数据的读取和存储都经过高速缓存,CPU核心和高速缓存之间有一条快速通道,在上方简化的图中,主存和高速缓存都连接在系统总线上。

缓存容量远远小于主存,

一般二级缓存大于一级缓存容量,但速度比一级慢,三级缓存大于二级缓存,但是速度更慢。

1、为什么需要CPU cache ?

  答:CPU的频率太快了,快到主存跟不上,这样在处理器时钟周期内,CPU常常需要等待主存,浪费资源。所以cache的出现,是为了缓解CPU和内存之间速度的不匹配问题(结构:cpu——》cache——》memory)

2、CPU cache 有什么意义:

1)时间局部性:如果某个数据被访问,那么在不久的将来它很可能被再次访问;

2)空间局部性:如果某个数据被访问,那么与它相邻的数据很快也可能被访问;

二、CPU多级缓存——缓存一致性问题(MESI):

》用于保证多个CPU cache之间缓存共享数据的一致:

MESI是一个协议,为了保证多个CPU缓存共享数据的一致性的问题,定义了cache line的四种状态,

而CPU对cache的四种操作可能会产生不一致的状态,因此缓存控制器监听到本地 操作和远程操作的时候,

需要对地址异径的cache line的状态做出一定的修改,从而保证数据在多个缓存之间流转的一致性。

》》其中MESI 是四个单词的缩写,M 为 modified  表示修改     E: exclusive 表示独享   S :Shared  表示共享   I :invalid  表示无效

M :表示被修改,指该缓存行,值被缓存在该CPU的缓存中,并且是被修改过的。因此它与主存中的数据是不一样的,该缓存行中的内存需要在未来的某个时间点写回主存。这个时间点我们是允许其他CPU读取主存中相应的内存之前,当这里的值被写回主存之后呢,该缓存行的状态会变成E 的状态,就是独享。

E:独享状态的缓存行,值被缓存在该CPU的缓存中,它是未被修改过的,是和主存中的数据是一致的。这个状态,可以在任何时刻,当有其他CPU读取该内存时变成S状态,即共享状态。同样的,当CPU修改该缓存行的内容时,该状态可以变成M 状态,即modified 的状态。

S :共享状态,意味着该缓存行可能被多个CPU进行缓存,并且各个缓存中的数据与主存数据是一致的。当有一个CPU修改该缓存行时,其他CPU从该缓存行是可以被作废的,变成invalid的状态。

I :无效状态,就是该缓存行的数据是无效的,可能是有其他CPU修改了该缓存行。

》》四种操作包括:

local read: 读本地缓存中的数据

local write:将数据写入到本地缓存中

remote read:将内存中的数据读取过来

remote write :将数据写回到主存中去。

要完整理解MESI协议,将四种操作和四种状态组成的16种状态转换考虑清楚。

在多核系统中,每个核都有自己的缓存来共享主存总线,每个相应的CPU会发出读写请求,而缓存的目的是为了减少CPU读写共享主存的次数。一个缓存除了在invalid的状态之外,都可以满足CPU的读请求。比如下图,除了在invalid的状态下,都是可以执行local read、 remote read操作。而一个写请求,只有在该缓存行是M 状态或者E 状态才能被执行。如果该缓存行当前状态是S状态时,必须先将缓存中的该缓存行的状态变为无效状态,这个操作通常用广播的方式来完成。这时既不允许不同的CPU同时修改同一个缓存行,即使修改同一个缓存行不同位置的不同数据也是不允许的。这里主要是解决缓存不一致的问题。

 一个处于M 状态的缓存行,必须时刻监听所有试图读该缓存行相对读主存的操作,这种操作必须在缓存将该缓存行写回到主存,并将状态变成S状态之前被延迟执行。

一个处于S 状态的缓存行,必须监听其他缓存使该缓存行无效或者独享该缓存行的请求,并将该缓存行置为无效。

一个处于E状态的缓存行,它要监听其他缓存读缓存中该缓存行的操作,一旦有该缓存行的操作,就要变为S共享状态。因此对于M 和E 两种状态而言,数据总是jingqu的。他们在和缓存行的状态总是一致的。而S状态可能是不一致的。如果一个缓存将处于S状态的缓存行作废,另一个缓存实际上可能已经独享了该缓存行,但该缓存却不会将该缓存行升迁为E状态,这是因为其他缓存不会广播他们作废掉该缓存行的通知。同样,因为该缓存没有保存该缓存行copy的数量,因此也没有办法确定该缓存行是否被自己独享了。从上面的意义来看,E状态更像是一种投机性的优化。因为CPU想修改一个处于S 共享状态的缓存行,总线事务需要将所有copy的值变为invalid的状态才可以。而修改E状态的缓存,却不需要使用总线事务。

二、CPU多级缓存——乱序执行优化

》处理器为提高运算速度而做出违背代码原有顺序的优化;

如上,为了计算a*b这个式子,计算机在赋值时,可能不是按照初始的先 a=10,b=200,而是有可能是先b=200,后a=10类似这种。单核时代,这种先后不会使得结果出现异常,但是多核时代有可能出现异常。比如后赋值的变量可能不是最后被赋值。

比如多核中,在一个核上执行数据写入操作,并在最后写一个标记,表示之前的数据已经准备好了。然后在另一个核上判断该标记标记的数据是否已经准备好了,这就存在一定的风险,比如,标记的数据先被写入,但实际上数据还未完成,这个完成,可能是计算未完成,也可能是数据没有从缓存刷新到主存当中。最终导致另一个核读取到了错误的数据。

三、java内存模型(Java Memory Model, JMM):

为了保证在不同平台下实现相同并发的效果,java规范中定义了内存模型。

JMM是一种规范,它规定了java虚拟机与计算机内存之间是如何协同工作的,它规定了一个线程如何和何时可以看到其他线程修改过后的共享变量的值,以及在必需时,如何同步地访问共享变量。

 堆:是运行时的一个数据区,它的数据由java垃圾回收器回收,

        优势:可以在运行时动态地分配内存大小,生存期也不必事先告诉编译器。因为它是在运行时动态地分配内存的。java垃圾回收器会自动回收不再需要的数据。

         劣势:由于是要在运行时动态分配内存,因此存取速度比较慢。

栈:和堆相比,

      优势:存取速度快,仅次于计算机中的寄存器。它可以共享

      缺点:存放的数据必须是类型、大小和生存期必须是确定的,否则编译会报错,缺乏一定的灵活性。栈中主要存放java一些基本类型变量,比如小写的int double String 等。JAVA 内存模型JMM要求调用栈和本地变量存放在线程栈上,对象存放在堆上。一个本地变量可能指向一个对象的引用,此时引用放在线程栈上,而对象本身实际存放在堆上。一个对象的成员变量随着对象本身存放在堆上,静态成员变量随着类的定义存放在堆上。存放在堆上的对象可以被持有该对象引用的线程访问。当一个线程可以访问一个对象时,也可以访问该对象的成员变量。如果两个线程同时调用同一个对象的同一个方法,它们都会访问这个对象的成员变量,但是每一个线程都拥有了这个成员变量的私有拷贝,这点非常重要!!

1、计算机硬件架构图:

 多CPU,一个CPU可能还有多个核。如果你的java程序运行在多CPU计算机上,同时运行多个线程是完全有可能的。每个CPU运行一个线程是没问题,一个java程序运行在多CPU的环境中,并发是非常有可能的。

CPU的寄存器是CPU内存的基础,每个CPU包含一系列的寄存器。CPU在寄存器上执行的速度远大于主存,因为CPU访问寄存器的速度远大于主存.

高速缓存,CPU的频率远大于主存,为了提高和保证运行速度,建立了运行速度接近于CPU的高速缓存,作为CPU和主存之间的缓冲。当计算时,把需要的数据放到缓存中,让运算能快速进行,运算结束后,再把数据从高速缓存同步到主存当中,这样就不用让CPU等待内存缓慢的读写了。在某个时刻,可能有一个或者可能会有多个缓存行被读到缓存,也可能有一个或者多个缓存行被刷回到主存中。也就是同一个时刻,可能有多个操作。

所有的CPU都可以访问主存,通常比CPU的高速缓存大地多。

2、java内存模型和硬件架构模型之间的关联图分析:

硬件模型没有区分栈和堆等,对于硬件而言,所有的java内存模型中提到的线程堆和栈都分布在主存中,虽然有时候,栈和堆会出现在CPU的高速缓存或者CPU寄存器中。

 

 共享变量是存放在CPU的主存中的,而上图中本地内存A、B是抽象的,不是真实存在的,是java线程中的逻辑概念。每个线程都有一个私有的本地内存,它涵盖了缓存、写缓存区、寄存器以及其他硬件和编译器的优化。本地线程存储了一个共享变量的副本。即如果线程要访问一个共享变量,它就拷贝该共享变量一个副本放在本地内存中。从更低的层次来说,主内存就是硬件的内存。而为了获取更好的运行速度,虚拟机及硬件系统可能会让工作内存优先存放在CPU的寄存器和高速缓存中,JMM中的线程的工作内存是CPU中的寄存器和高速缓存的一个抽象的描述,而JVM java 静态内存存储模型,只是一种对内存的物理划分而已,它只限于内存,而且只局限在jvm的内存。现在如果线程间通信,必须要经历主内存,即线程A 从内存中读取数据到本地高速缓存,再到寄存器,计算完毕,再将数据从CPU的寄存器刷新到高速缓存,再刷新到主存中。而线程B 也可能这样的操作。

3、所以分析之前的,多线程,对一个共享变量执行累加操作时会发生异常,原因就是,

线程A 在进行如上读取内存数据到本地,计算执行完毕再刷新到主存中时,B线程也在这么操作,而两个线程之间因为操作不可见,造成数据没有及时同步,导致结果不符合预期。比如A 执行完,写回到内存后,B线程即将也要写数据到内存中,但写数据到内存前,未确认(在B 读取 到本地、计算、 刷新到主存这个过程中)是否有其他线程有修改过该数据。

4、对此要提出一些概念规则。java内存同步方法和规则。

》》》java内存模型——同步的八种操作:

》lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态;

》unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定;

》read(读取):作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用;

》load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中;

》use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎;

》assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量;

》store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作;

》write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。

》》》java内存模型——同步规则:

》如果要把一个变量从主内存中复制到工作内存,就需要按顺序地执行read 和load操作,如果把变量从工作内存中同步回主内存中,就要按顺序地执行store和write操作。但java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行

》不允许read和load 、store和write操作之一单独出现;

》不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中;

》不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中;

》一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。

》一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。lock和unlock必须成对出现;

》如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值。

》如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许unlock一个被其他线程锁定的变量;

》对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作);

5、并发的优势与风险: 

通常网络io或者磁盘io 比CPU和内存的io缓慢很多。

总结:

》CPU多级缓存:缓存一致性、乱序执行优化;

》java内存模型:JMM规定、抽象结构、同步八种操作及规则;

》并发的优势与风险;

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值