并发编程(一)内存模型

        开始挖坑并发编程系列文章,那为什么要有并发编程,我们都知道是为了提高CPU的利用率:有的线程是CPU密集型,有的线程是IO密集型。在单核CPU的情况下,有时候为了提高IO密集型线程对CPU的利用率,采用多线程策略,让操作系统调度。如果是多核CPU,更要多个线程,利用每一个核。CPU的核数和线程个数的设置是有关系,既不要让CPU空余时间太多,又不要撑爆CPU,可以采用以下算法,也可以采用压测法,根据测试环境的数据去类比生产环境,进行评估。
        多线程场景中的并发问题包括可见性、原子性、有序性,为什么会有这些问题?如何解决呢?

计算机内存模型

        在这之前,我们先说下计算机硬件的升级:计算机处理数据是用处理器CPU运算,用到的数据是从内存中加载读取,但是内存的读写速度远不及计算机CPU的运算速度,这就导致CPU每次操作内存都需要耗费很多时间等待,所以为了提高计算机的处理速度,在处理器CPU和主内存之间加了一层高速缓存(特点是速度快、内存小、价格贵)。那么程序运行的时候,会将主内存的数据copy一份放入高速缓存,CPU运算时直接读取高速缓存的数据,运算结束后,再将高速缓存的数据刷新回主内存。计算机硬件升级也带来了问题:

1、缓存一致性问题: 

        如果多核CPU处理器,共同访问同一块主内存的数据,那运算处理后, 各自高速缓存区的数据结果有可能不一致,那写会到主内存的时候,以谁的数据为准呢?所以这就涉及问题:缓存一致性。
        如何做:好在缓存一致性可以通过让CPU读写缓存时遵守一些协议来解决,比如这些缓存协议:MSI、MESI、MOSI、Synapse、Firefly、DragonProtocal。比较出名的是MESI协议,MESI(Modified Exclusive Share Invalid)(也称伊利诺斯协议)是一种广泛使用的支持写回策略的缓存一致性协议,该协议被应用在 Intel 奔腾系列的 CPU 中。当一个 CPU 修改了 高速缓存中的数据,会通知其他缓存了这个数据的 CPU,其他 CPU 会把 自己高速缓存 中这份数据置为无效,要读取数据的话,直接去内存中获取,不会再从缓存中获取了。

2、CPU处理器资源浪费问题:

        MESI协议解决了缓存一致性问题,但留有bug。比如多核CPU下共享数据,如果其中一个CPU1对数据进行了修改,MESI协议会通知其他CPU的cache,在这个过程中,需要同步的通知多个缓存,我们的CPU1一直都是处于等待状态,这也降低了CPU的性能。

        如何做:为了避免这种CPU处理器运算能力的浪费,引入Store Bufferes 概念,将 cpu 写入主存从同步阻塞变为异步,大大提高了 cpu 执行效率。

3、指令重排问题:

        处理器优化会导致指令重排问题,注意并不是代表指令的位置被更改,而是CPU读入的顺序和程序中的顺序不一致。

        如何解决:使用内存屏障指令,内存屏障分为以下几种:

读屏障load Barrier:在读取指令前插入读屏障,让缓存中的数据失效,重新从主内存中加载数据,保证数据是最新的。

写屏障store Barrier,在写指令后插入写屏障,同步把缓存的数据写回主内存,保证其他缓存立即可见。

        至此,计算机发展中遇到的问题都一一解决,而这一系列问题解决方案,都是内存模型规范的。内存模型,它是一个规范,它定义了共享内存系统中多线程程序对内存读写操作行为的规范。

Java内存模型

        因为不同的物理机器,计算机硬件“内存模型”有可能不一样。而Java虚拟机中定义的Java内存模型(Java Memory Model 简称JMM)可以屏蔽不同的硬件内存模型差异,保证Java程序在不同的平台都能运行,实现一次编写到处运行。

        JMM规定所有的变量是存在主内存,但是线程不能直接操作主内存数据,而是操作在自己的工作内存中保留一份用到的数据副本,线程处理完后再把工作内存的数据同步回主内存,JMM负责工作内存和主内存的数据同步,什么时候同步,如何同步工作。

        Java内存模型中的主内存和工作内存,可以类比成计算机内存模型中的主内存和高速缓存的概念。计算机内存模型可以看做Java内存模型的一个抽象概念,Java内存模型可以看做一种规范的具体实现。关于主内存和工作内存之间的具体交互协议,即数据如何从主内存和工作内存之间读入和回写的呢?Java内存模型定义了8种操作来完成:

  • lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。
  • unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  • read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便后的load动作使用。
  • load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
  • use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  • store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
  • write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。

Java内存模型规定执行这8种操作,必须遵守以下规则:

  • 不允许 read 和 load、store 和 write 操作之一单独出现,也就是不允许从主内存读取了变量的值但是工作内存不接收的情况,或者不允许从工作内存将变量的值回写到主内存但是主内存不接收的情况
  • 不允许一个线程丢弃最近的 assign 操作,也就是不允许线程在自己的工作线程中修改了变量的值却不同步/回写到主内存
  • 不允许一个线程回写没有修改的变量到主内存,也就是如果线程工作内存中变量没有发生过任何 assign 操作,是不允许将该变量的值回写到主内存
  • 变量只能在主内存中产生,不允许在工作内存中直接使用一个未被初始化的变量,也就是没有执行 load 或者 assign 操作。也就是说在执行 use、store 之前必须对相同的变量执行了 load、assign 操作
  • 一个变量在同一时刻只能被一个线程对其进行 lock 操作,也就是说一个线程一旦对一个变量加锁后,在该线程没有释放掉锁之前,其他线程是不能对其加锁的,但是同一个线程对一个变量加锁后,可以继续加锁,同时在释放锁的时候释放锁次数必须和加锁次数相同。
  • 对变量执行 lock 操作,就会清空工作空间该变量的值,执行引擎使用这个变量之前,需要重新 load 或者 assign 操作初始化变量的值
  • 不允许对没有 lock 的变量执行 unlock 操作,如果一个变量没有被 lock 操作,那也不能对其执行 unlock 操作,当然一个线程也不能对被其他线程 lock 的变量执行 unlock 操作
  • 对一个变量执行 unlock 之前,必须先把变量同步回主内存中,也就是执行 store 和 write 操作

        Java内存模型不仅仅是定义一种规范,它还提供了一系列原语,封装了底层实现,供开发者直接调用,比如volatile、synchronized、final、concurren包等,详见后续。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值