从缓存入门到并发编程三要素详解 Java中 volatile 、final 等关键字解析案例

引入高速缓存概念

  1. 在计算机在执行程序时,以指令为单位来执行 , 每条指令都是在CPU中执行的,而执行指令过程中 , 势必涉及到数据的读取和写入 。
  2. 由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行指令的速度很快 , 而从内存读取数据和向内存写入数据的过程相对很慢 , 因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度 。因此就引入了高速缓存 。
  3. 特性:缓存(Cache memory)是硬盘控制器上的一块内存,是硬盘内部存储和外界接口之间的缓冲器 。
高速缓存作用呢?
  1. 预读取
    ?相当于提前加载,猜测你可能会用到硬盘相邻存储地址的数据,它会提前进行加载到缓存中,后面你需要时,CPU就不需要去硬盘读取数据,直接读取缓存中的数据传输到内存中就OK了 , 由于读取缓存的速度远远高于读取硬盘时磁头读写的速度 , 所以能够明显的改善性能 。
  2. 对写入动作进行缓存
    ?硬盘接到写入数据的指令之后,并不会马上将数据写入到盘片上,而是先暂时存储在缓存里,然后发送一个“数据已写入”的信号给系统 , 这时系统就会认为数据已经写入,并继续执行下面的工作,而硬盘则在空闲(不进行读取或写入的时候)时再将缓存中的数据写入到盘片上 。
  3. 换到应用程序层面也就是,当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据同步到主存当中 。
举个简单的例子,比如下面的这段代码:
【从缓存入门到并发编程三要素详解 Java中 volatile 、final 等关键字解析案例】i = i + 1;
  • 当线程执行这个语句时,会先从主存当中读取i的值,然后复制一份到高速缓存当中,然后CPU执行指令对i进行加1操作,然后将数据写入高速缓存 , 最后将高速缓存中i最新的值刷新到主存当中 。
  • 这个代码在单线程中运行是没有任何问题的,但是在多线程中运行就会有问题了(存在临界区) 。在多核CPU中,每条线程可能运行于不同的CPU中,因此每个线程运行时有自己的高速缓存区(对单核CPU来说,其实也会出现这种问题 , 只不过是以线程调度的形式来分别执行的) 。
比如有两个线程像下列执行顺序:
  1. 线程一执行 i = i + 1,线程二执行var = i
  2. 线程二此时去主存中获取变量 i,线程一只是在高速缓存中更新了变量,还未将变量i写会主存
  3. 线程二读到的i不是最新值,此时多线程导致数据不一致
?类似上面这种情况即为缓存一致性问题 。读写场景、双写场景都会存在缓存一致性问题,但读读不会 。前提是需要在多线程运行的环境下,并且需要多线程去访问同一个共享变量 。
?这里的共享又可以回到上文中,即为上面所说,他们每个线程都有自己的高速缓存区,但是都是从同一个主存同步获取变量 。
那么这种问题应该怎样解决呢?
解决缓存不一致问题(硬件层面)
  1. 总线加锁模式
    • 由于CPU在执行命令和其他组件进行通信的时候都需要通过总线,倘若对总线加锁的话,线程一执行i = i + 1 整个命令过程中,其他线程是无法访问主存的 。
    • 优缺只有一个,可以解决本问题;缺点的话除了优点全是缺点,效率低,成本高·····(谁也不会让一个主存同时只能干一件事)
  2. 缓存一致性协议
    • 协议可以保证每个缓存中使用的共享变量的副本是一致的 , 原理:CPU对主存中的共享变量有写入操作时,会立即通知其他CPU将该变量缓存行置为无效状态 。其他CPU发现该变为无效状态时,就会重新去主存中读取该变量最新值 。
    • 优点就是可以解决问题,读多写少效率还OK;缺点就是实现繁琐,较耗费性能,在对于写多的场景下效率很不可观
? 问题:线程为什么会不安全?
?答:共享资源不能及时同步更新,归根于 分时系统上下文切换时 指令还未执行完毕 (没有写回结果) 更新异常
引入并解释并发编程特性?众所周知现在的互联网大型项目 , 都是采用分布式架构同时具有其“三高症状” , 高并发、高可用、高性能 。高并发为其中最重要的特性之一,在高并发场景下并发编程就显得尤为重要,其并发编程的特性为原子性、可见性、有序性 。

推荐阅读