并发编程之JMM与并发三大特性

JMM 内存模型
在这里插入图片描述

JMM 8大原子指令在这里插入图片描述

什么是可见性问题?
当前线程对共享变量的操作会存在读不到,或者不能立即读到另一个线程对此变量的写操作

JAVA保证可见性的两种方式

  1. 内存屏障
    volatile 基于jvm的storeload内存屏障
  2. 上下文切换
    Thread.yield(); 上下文切换时会释放时间片,当下一次获取时间片时会从程序计数器取当前指令,并从主内存重新加载数据
    volatile可以保证可见性和有序性,但不能保证原子性。在32位处理器中,对于long、double这种64位的写操作,会分为高32位和低32位处理。(在java规范文档第17章有描述)

DCL(Double Check Lock双重检查锁)为什么要使用volatile?

public class DoubleCheckLock {
    private static DoubleCheckLock instance;

    private DoubleCheckLock(){}

    private static DoubleCheckLock getInstance() {
     // 第一次检查
        if (instance == null) {
            synchronized (DoubleCheckLock.class) {
                if (instance == null) {
              //多线程环境下可能出问题
                    instance = new DoubleCheckLock();
                }
            }
        }
        return instance;
    }
}

instance = new DoubleCheckLock();这个步骤实际分为三步:

  1. 开辟内存空间
  2. 对象初始化
  3. instance 指向内存空间地址

因为JMM不能保证多线程在临界区内的重排序。如果重排序让步骤3先于步骤2执行,对于当前Thread-1来说没问题,但是对于后面来的Thread-2,进行第一次检查时发现instance != null,就会继续往下执行。但是instance此时只有内存地址,这片空间里是空的。Thread-2会抛出NullPointException。

CPU组成部分:
](https://img-blog.csdnimg.cn/b97483f00f1a407193f41d0eb8c44127.png)

  1. ALU: cpu 的算术逻辑单元
  2. Register: 寄存器。将ALU计算得出的值存到寄存器中,最后回写到内存,因为其他线程拿这个值得去内存中获取
  3. PC:程序计数器,存储线程当前执行的指令
  4. cache: 高速缓存

执行流程:计算 x=3, y=x+5,在CPU中执行的顺序
PC先取指令,然后从寄存器中找x的值,不存在则去cache中获取,cache中也不存在则去内存中获取。从内存将x=3加载到缓存,再加载到寄存器,再去PC取指令add,然后去ALU计算

CPU高速缓存cache : 容量比寄存器大,但比内存小,
作用:

  1. 减少cpu等待时间
  2. 提升cpu计算能力(局部性原理:时间局部性和空间局部性,取数据时将一片连续的内存空间加载到缓存中,因为底层认为连续区域的数据会立刻用到,比如mysql引擎加载数据时一次取4K的整数倍,缓存命中的概率大)

CPU有三级缓存,一级比一级大 L1 < L2 < L3
速度是L1 > L2 > L3 > 内存,L3是多核心共享的缓存
一个cpu核中,L1有两个,一个缓存指令,一个缓存数据
在这里插入图片描述在这里插入图片描述

这些缓存优化都是为了减少cpu等待时间,提升性能
内存的速度远远跟不上cpu的主频的速度的。
如果将cpu存取一个指令作为一个时钟周期,比如指令x=3,执行一个load指令, 从内存中加载到寄存器需要167个时钟周期,而从L1缓存加载只需要等待4个时钟周期
在这里插入图片描述

内存屏障
作用:禁止指令重排序、刷新处理器缓存
分类:JMM层面、JVM层面、硬件层面

硬件层面:fence族指令

  1. lfence: load barrier 读屏障
  2. sfence: store barrier 写屏障
  3. mfence: 全能型屏障,具备lfence和sfence的能力
  4. Lock前缀指令: Lock不是一种内存屏障,但它能完成类似内存屏障的功能,Lock会对CPU总线和高速缓存加锁,可以理解为CPU指令级的一种锁。

JVM层面:JSR规范中定义了四种内存屏障

  1. LoadLoad屏障:(指令Load1; LoadLoad; Load2),在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
  2. LoadStore屏障:(指令Load1; LoadStore; Store2),在store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
  3. StoreStore屏障:(指令Store1; StoreStore; Store2),在store2及后续写入操作执行前,保证store1的写入对所有处理器可见。
  4. StoreLoad屏障:(指令Store1; StoreLoad; Load2),在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见,它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其他三种内存屏障的功能。

由于x86只有storeload可能会重排序,所以只有JSR的StoreLoad屏障对应它的mfence或lock前缀指令,其他屏障对应空操作。

JMM层面:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
缓存一致性协议
MESI
M: modify 修改
E: Exclusive 独占
S: Share 共享
I: invalid 失效

  1. 总线锁定:在早期的处理器中,解决缓存不一致用的是总线锁定,即CPU串行执行,锁定整个内存。会使导致性能严重下降
    使用的是Lock # 和Lock前缀指令结合来实现的
  2. 缓存锁定:在现代的处理器中,使用缓存锁定,将一个64字节的缓存行锁定。当一个处理器loads或stores一个内存地址x时,它会在bus总线上广播该请求,
    其他的处理器和主内存都会监听总线(也称为snooping)让缓存立即刷回主内存,减少其他核的等待时间

执行流程:

  1. CPU1从内存中将变量x加载到缓存中,并将变量x的状态改为E(独享),并通过总线嗅探机制对内存中变量x的操作进行嗅探
  2. 此时,CPU2读取变量x,总线嗅探机制会将CPU1中的变量x的状态置为S(共享),并将变量x加载到CPU2的缓存中,状态为S
  3. CPU1对变量x进行修改操作,此时CPU1中的变量x会被置为M(修改)状态,而CPU2中的变量x会被通知,改为I(无效)状态,
    此时CPU2中的变量x做的任何修改都不会被写回内存中(高并发情况下可能出现两个CPU同时修改变量a,并同时向总线发出
    将各自的缓存行更改为M状态的情况,此时总线会采用相应的裁决机制进行裁决,将其中一个置为M状态,另一个置为I状态,且I状态的缓存行修改无效)
  4. CPU1将修改后的数据写回内存,并将变量x置为E(独占)状态
  5. 此时,CPU2通过总线嗅探机制得知变量xa已被修改,会重新去内存中加载变量x,同时CPU1和CPU2中的变量x都改为S状态
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值