高并发编程-通过volatile重新认识CPU缓存 和 Java内存模型(JMM)

简介: 高并发编程-通过volatile重新认识CPU缓存 和 Java内存模型(JMM)

20191031000638293.png


概述


在多线程并发编程中synchronized和volatile都扮演着重要的角色。 volatile是轻量级的 synchronized,它在高并发中保证了共享变量的“可见性”。


那什么是可见性呢?


可见性 我们可以理解为:当一个线修改一个共享变量时,另外一个线程能读到这个修改的值。


如果volatile变量修饰符使用恰的话,它比synchronized的使用和执行成本更低,因为volatile不会引起线程上下文的切换和调度


volatile定义


Java规范第3版中对volatile的定义如下:Java允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。


Java提供了volatile关键字,在某些场景下volatile比锁synchronized要更加方便。如果一个字段被声明成volatile,Java线程内存模型(JMM)确保所有线程看到这个变量的值是一致的 .


CPU缓存

相关CPU术语

了解volatile实现原理之前,先了解下与其实现原理相关的CPU术语


image.png

CPU缓存一致性协议MESI


CPU缓存一致性协议MESI 请参考: CPU缓存一致性协议MESI

【M 修改 (Modified) E 独享、互斥 (Exclusive) S 共享 (Shared) I 无效 (Invalid) 】


CPU的发展速度非常快,而内存和硬盘的发展速度远远不及CPU。这就造成了高性能能的内存和硬盘价格及其昂贵。然而CPU的高度运算需要高速的数据。为了解决这个问题,CPU厂商在CPU中内置了少量的高速缓存以解决I\O速度和CPU运算速度之间的不匹配问题

20191029151025349.png

为了提高效率,CPU不直接和内存进行通信,而是先将系统内存的数据读取到内部缓存(L1、L2或其他)后再进行操作。


但是有个问题: 当操作完成后,被修改的数据何时回写到主内存呢?


假设某个共享变量声明了volatile关键字进行写操作 ,JVM就会向处理器发送一条Lock前缀指令,将这个变量所在缓存行的数据写回到系统内存。


OK,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。


所以,在多处理器下,为了保证各个处理器的缓存是一致的,就要实现缓存一致性协议 ,每个处理器通过嗅探在总线(BUS)上传播的数据来检查自己缓存的值是不是过期了


  • 当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,
  • 当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存

带有高速缓存的CPU执行计算的流程


  1. 程序以及数据被加载到主内存
  2. 指令和数据被加载到CPU的高速缓存
  3. CPU执行指令,把结果写到高速缓存
  4. 高速缓存中的数据写回主内存


20191029150710461.png


CPU 多级的缓存结构


由于CPU的运算速度超越了1级缓存的数据I\O能力,CPU厂商又引入了多级的缓存结构


20191029150755530.png

L1/L2/L3 Cache速度差别

L1 cache: 3 cycles

L2 cache: 11 cycles

L3 cache: 25 cycles

Main Memory: 100 cycles

通常L1 Cache离CPU核心需要数据的地方更近,而L2 Cache则处于边缓位置,访问数据时,L2 Cache需要通过更远的铜线,甚至更多的电路,从而增加了延时。

参见: 细说Cache-L1/L2/L3/TLB


Java 内存模型 (JMM)


线程通信的两种方式


我们知道 线程间的通信,主要分为两种方式


  1. 共享内存 (线程之间共享程序的公共状态,通过写-读内存中的公共状态进行隐式通信)
  2. 消息传递 (线程之间必须通过发送消息来显式进行通信)


哪些变量可以共享


Java的并发采用的是共享内存模型 , 在Java中,所有实例域、静态域和数组元素都存储在堆内存中,堆内存在线程之间共享 ,我们使用”共享变量”这个术语代指实例域,静态域和数组元素


局部变量,方法定义参数和异常处理器参数不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的影响。


JMM概述


Java线程之间的通信由Java内存模型JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见 .


JMM定义了线程和主内存之间抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。


注: 本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器化


Java内存模型的抽象结构示意图

如下:



20191028231218576.png


根据上述的描述,如果线程A和线程B要通信的话,步骤如下

  1. 线程A把本地内存A中更新过的共享变量刷新到主内存中去
  2. 线程B到主内存中去读取线程A之前已更新过的共享变

线程A和线程B通信示意图如下所示



20191028231921652.png


本地内存A和本地内存B由主内存中共享变量x的副本。


假设初始时,这3个内存(本地内存A、本地内存B、主内存)中的x值都为0。

线程A在执行时,把更新后的x值(假设值为1)临时存放在自己的本地内A中。

当线程A和线程B需要通信时,线程A首先会把自己本地内存中修改后的x值刷新到主内存中,此时主内存中的x值变为了1。

随后,线程B到主内存中去读取线程A更新后的x值,此时线程B的本地内存的x值也变为了1。

从整体来看,这两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存。JMM通过控制主内存与每个线程的本地内存之间的交互,来保证内存可见性保证。


volatile 小demo


先来个例子 感受下volatile的作用

俩线程 1个读取共享变量 另外1个更新共享变量.

package com.artisan.test;
/**
 * 俩线程
 * <p>
 * 1个读取共享变量
 * 1个更新共享变量
 */
public class VolatileDemo {
    // 共享变量
    private volatile static int SHARED_VALUE = 0;
    private final static int MAX_VALUE = 10;
    public static void main(String[] args) {
        // 定义 读取线程
        new Thread(() -> {
            int localValue = SHARED_VALUE;
            // 循环, 如果localValue != SHARED_VALUE 输出信息
            while (SHARED_VALUE < MAX_VALUE){
                if (localValue != SHARED_VALUE){
                    System.out.printf(Thread.currentThread().getName() + ": the SHARED_VALUE value has been updated to [%d]  \n" , SHARED_VALUE);
                    localValue = SHARED_VALUE;
                }
            }
        }, "Reader Thread").start();
        // 定义 更新线程
        new Thread(() -> {
            int localValue = SHARED_VALUE;
            // 循环 如果小于最大值,则更新localValue
            while (SHARED_VALUE < MAX_VALUE){
                System.out.printf(Thread.currentThread().getName() + ": update SHARED_VALUE  to [%d] \n" , ++localValue);
                SHARED_VALUE = localValue;
                try {
                    // 为了演示效果,休眠一下
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "Update Thread").start();
    }
}


輸出:

Update Thread: update SHARED_VALUE  to [1] 
Reader Thread: the SHARED_VALUE value has been updated to [1]  
Update Thread: update SHARED_VALUE  to [2] 
Reader Thread: the SHARED_VALUE value has been updated to [2]  
Update Thread: update SHARED_VALUE  to [3] 
Reader Thread: the SHARED_VALUE value has been updated to [3]  
Update Thread: update SHARED_VALUE  to [4] 
Reader Thread: the SHARED_VALUE value has been updated to [4]  
Update Thread: update SHARED_VALUE  to [5] 
Reader Thread: the SHARED_VALUE value has been updated to [5]  
Update Thread: update SHARED_VALUE  to [6] 
Reader Thread: the SHARED_VALUE value has been updated to [6]  
Update Thread: update SHARED_VALUE  to [7] 
Reader Thread: the SHARED_VALUE value has been updated to [7]  
Update Thread: update SHARED_VALUE  to [8] 
Reader Thread: the SHARED_VALUE value has been updated to [8]  
Update Thread: update SHARED_VALUE  to [9] 
Reader Thread: the SHARED_VALUE value has been updated to [9]  
Update Thread: update SHARED_VALUE  to [10] 
Process finished with exit code 0


如果 去掉volatile关键字呢 ? 测试如下


20191027233937759.png



由此可见 volatile关键字在高并发中保证了共享变量的“可见性”。


总结:volatile的两条实现原则

总结一下

  • Lock前缀指令会引起处理器缓存回写到内存
  • 一个处理器的缓存回写到内存会导致其他处理器的缓存无效


相关文章
|
2月前
|
存储 缓存 安全
Java内存模型深度解析:从理论到实践####
【10月更文挑战第21天】 本文深入探讨了Java内存模型(JMM)的核心概念与底层机制,通过剖析其设计原理、内存可见性问题及其解决方案,结合具体代码示例,帮助读者构建对JMM的全面理解。不同于传统的摘要概述,我们将直接以故事化手法引入,让读者在轻松的情境中领略JMM的精髓。 ####
47 6
|
1月前
|
安全 Java 程序员
深入理解Java内存模型与并发编程####
本文旨在探讨Java内存模型(JMM)的复杂性及其对并发编程的影响,不同于传统的摘要形式,本文将以一个实际案例为引子,逐步揭示JMM的核心概念,包括原子性、可见性、有序性,以及这些特性在多线程环境下的具体表现。通过对比分析不同并发工具类的应用,如synchronized、volatile关键字、Lock接口及其实现等,本文将展示如何在实践中有效利用JMM来设计高效且安全的并发程序。最后,还将简要介绍Java 8及更高版本中引入的新特性,如StampedLock,以及它们如何进一步优化多线程编程模型。 ####
36 0
|
1月前
|
安全 Java 程序员
Java内存模型的深入理解与实践
本文旨在深入探讨Java内存模型(JMM)的核心概念,包括原子性、可见性和有序性,并通过实例代码分析这些特性在实际编程中的应用。我们将从理论到实践,逐步揭示JMM在多线程编程中的重要性和复杂性,帮助读者构建更加健壮的并发程序。
|
2月前
|
Java
Java内存模型
JMM(Java内存模型 )屏蔽了各种硬件和操作系统的内存访问差异,实现让Java程序在各平台下都能达到一致的内存访问效果,它定义了JVM如何将程序中的变量在主存中读取 具体定义为:所有变量都存在主存中,主存是线程共享区域;每个线程都有自己独有的工作内存,线程想要操作变量必须从主从中copy变量到自己的工作区,每个线程的工作内存是相互隔离的 由于主存与工作内存之间有读写延迟,且读写不是原子性操作,所以会有线程安全问题
|
2月前
|
缓存 算法 Java
本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制
在现代软件开发中,性能优化至关重要。本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制。通过调整垃圾回收器参数、优化堆大小与布局、使用对象池和缓存技术,开发者可显著提升应用性能和稳定性。
56 6
|
2月前
|
存储 缓存 安全
Java内存模型(JMM):深入理解并发编程的基石####
【10月更文挑战第29天】 本文作为一篇技术性文章,旨在深入探讨Java内存模型(JMM)的核心概念、工作原理及其在并发编程中的应用。我们将从JMM的基本定义出发,逐步剖析其如何通过happens-before原则、volatile关键字、synchronized关键字等机制,解决多线程环境下的数据可见性、原子性和有序性问题。不同于常规摘要的简述方式,本摘要将直接概述文章的核心内容,为读者提供一个清晰的学习路径。 ####
52 2
|
2月前
|
存储 安全 Java
什么是 Java 的内存模型?
Java内存模型(Java Memory Model, JMM)是Java虚拟机(JVM)规范的一部分,它定义了一套规则,用于指导Java程序中变量的访问和内存交互方式。
108 1
|
6月前
|
存储 安全 Java
Java面试题:请解释Java内存模型(JMM)是什么,它如何保证线程安全?
Java面试题:请解释Java内存模型(JMM)是什么,它如何保证线程安全?
124 13
|
6月前
|
Java 程序员 编译器
Java面试题:解释Java内存模型(JMM)是什么,它为何重要?
Java面试题:解释Java内存模型(JMM)是什么,它为何重要?
86 2
|
6月前
|
设计模式 安全 Java
Java面试题:设计模式如单例模式、工厂模式、观察者模式等在多线程环境下线程安全问题,Java内存模型定义了线程如何与内存交互,包括原子性、可见性、有序性,并发框架提供了更高层次的并发任务处理能力
Java面试题:设计模式如单例模式、工厂模式、观察者模式等在多线程环境下线程安全问题,Java内存模型定义了线程如何与内存交互,包括原子性、可见性、有序性,并发框架提供了更高层次的并发任务处理能力
99 1