资料总结 资料总结
首页
go
java
云原生
  • mysql
  • redis
  • MongoDB
  • 设计模式详解
  • 数据结构与算法
  • 前端
  • 项目
  • 理论基础
  • 运营
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

linghui Wu

一只努力学飞的鱼
首页
go
java
云原生
  • mysql
  • redis
  • MongoDB
  • 设计模式详解
  • 数据结构与算法
  • 前端
  • 项目
  • 理论基础
  • 运营
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • java-se

    • java集合
    • 计算机基础
    • 并发编程
      • Java内存模型(JMM)
        • JMM存在的必要性
        • JMM结构和工作原理
        • 线程,工作内存,主内存工作交互图(JMM规范)
        • 主内存
        • 工作内存
        • 主内存与工作内存的数据存储类型以及操作方式
        • Java内存模型与硬件内存架构的关系
        • 并发编程的可见性,原子性与有序性问题
        • JMM如何解决原子性&可见性&有序性问题
        • JMM数据同步
        • 八大原子操作
        • 流程
        • 同步规则分析
      • 临界区
      • 竞态条件
      • volatile内存语义
        • 作用
        • volatile的可见性
        • volatile无法保证原子性
        • 指令重排序
        • volatile禁止重排优化
        • 内存语义的实现
        • 硬件层的内存屏障
      • 参考资料
    • java线程
    • java协程
    • synchronized
    • Unsafe&Atomic
    • AQS
    • Lock
    • JUC工具杂记
    • Queue
    • 线程池原理
    • Future
    • ForkJoin
    • BIO,NIO,AIO
  • jvm

  • mybatis

  • Netty

  • 爬虫 webmagic

  • spring

  • spring-cloud

  • 中间件

  • flowable

  • idea工具

  • maven

  • ms

  • java部署

  • 原生安卓

  • java
  • java-se
wulinghui
2022-02-10
目录

并发编程

# Java内存模型(JMM)

# JMM存在的必要性

  1. 简化多线程编程
  • 不需要知道底层细节lock、mesi一致性协议、硬件缓存协议
  1. 保证程序可移植性
  • 依赖于处理器本身的内存一致性模型,但不同的处理器可能差异很大

  • 屏蔽掉不同操作系统中的内存差异性来保持并发的一致性

  1. 明确处理器层面,过于泛泛的内存模型定义,各个厂商硬件实现不一样

# JMM结构和工作原理

JMM不同于JVM内存区域模型

# 线程,工作内存,主内存工作交互图(JMM规范)

每个线程创建时 JVM都会为其创建一个工作内存(有些地方称为栈空间),用于存储线程私有的数据,而Java 内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行

# 主内存

主要存储的是Java实例对象,所有线程创建的实例对象都存放在主内存中,不管该实例 对象是成员变量还是方法中的本地变量(也称局部变量)

# 工作内存

  1. 工作内存是每个 线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必 须通过主内存来完成。
  2. 主要存储当前方法的所有本地变量信息,每个线程只能访问自己的工作内存,对其它线程是不可见的,所以存储在工作内存的数据不存在线程安全问题
  3. 将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本拷贝

# 主内存与工作内存的数据存储类型以及操作方式

  1. 方法中包含本地变量是基本数据类型,直接存储在工作内存的帧栈结构,而不放到主内存中
  2. 本地变量是引用类型,该引用存工作内存帧栈,对象实例将存储在主内存
  3. 成员变量,不管它是基本数据 类型或者包装类型(Integer、Double等)还是引用类型,都会被存储到主内存
  4. 两个线程同时调用了同一个对象的同一个方法,那么两条线程会将要操作 的数据拷贝一份到自己的工作内存中,执行完成操作后才刷新到主内存

# Java内存模型与硬件内存架构的关系

JMM只是一种抽象的概念,不管是工作内存的数据还是主内存的数据,对于计算机硬件来说都会存储在计算机主内存中,也有可能存储到CPU缓存或者寄存器中

# 并发编程的可见性,原子性与有序性问题

3大原则:

    1. 原子性 一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不 会被其他线程影响。
    2. 可见性 当一个线程修改了某个共享变量 的值,其他线程是否能够马上得知这个修改的值 编译器优化还是处理器优化的重排现象,在多线程环境下,确实会导致程序轮序执行的问题
    3. 有序性
    • 程序编译成机器码指令后可能会出现指令重排现象,重排后的指令与原指令的顺序未必一致,但是最终执行的结果不变.

# JMM如何解决原子性&可见性&有序性问题

实现3大原则:

  1. 原子性问题 synchronized和Lock实现原子性
  2. 可见性问题
  • synchronized和Lock也可以保证可见性,因为它们可以保证任一时刻只有一个线程能访问共享资源,并在其释放锁之前将修改的变量刷新到内存中。
  • volatile修改的值立即更新到主存中,同时清空其他线程工作内存的值
  • UnsafeFactory.getUnsafe().storeFence(); //内存屏障
  • Thread.yield(); // 释放时间片,上下文切换 加载上下文:flag=true
  • System.out.println(count); // 内存屏障
  • LockSupport.unpark(Thread.currentThread()); // 内存屏障
  • Thread.sleep(1); //内存屏障
  • 总结: Java中可见性如何保证? 方式归类有两种: jvm层面 storeLoad内存屏障 ; 上下文切换 Thread.yield();
  1. 有序性问题
  • 可以通过volatile关键字来保证一定的“有序性”
  • 通过线程同步synchronized和Lock来保证有序性
  1. 指令重排序
  • 目的:: 为了最大限度的 发挥机器性能,适当的对机器指令进行重排序,使机器指令能更符合CPU的执行特性。
  • 定义::: 只要程序的最终结果 与它顺序化情况的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的重排序。
  • as-if-serial语义 :: 不管怎么重排序,程序的执行结果不能被改变。
  • happens-before 原则
  1. 多线程间切换
    • 之前计算的值从哪来?? 线程切换之前会保存上下文, 再次运行时会还原上下文
    • 如何保证从那一行接着执行?? PC计数器记录

# JMM数据同步

# 八大原子操作

  • lock(锁定) 作用于主内存的变量,把一个变量标记为一条线程独占状态
  • unlock(解锁) 作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后 的变量才可以被其他线程锁定
  • read(读取) 作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存 中,以便随后的load动作使用
  • load(载入) 作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中
  • use(使用) 作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎
  • assign(赋值) 作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量
  • store(存储) 作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存 中,以便随后的write的操作
  • write(写入) 作用于工作内存的变量,它把store操作从工作内存中的一个变量的值 传送到主内存的变量中

# 流程

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

# 同步规则分析

  • 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中
  • 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化 (load或者assign)的变量。即就是对一个变量实施use和store操作之前,必须先自行 assign和load操作。
  • 一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一线程重 复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。lock和unlock必须成对出现。
  • 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个 变量之前需要重新执行load或assign操作初始化变量的值。
  • 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去 unlock一个被其他线程锁定的变量。如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去 unlock一个被其他线程锁定的变量。 volatile就是加了lock,来保证可见性的
  • 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write 操作)

# 临界区

  • 一个程序运行多个线程本身是没有问题的。

  • 当多个线程执行一个方法时,该方法内部的局部变量并不是临界资源,因为这些局部变量是在每个线程的 私有栈中,因此不具有共享性,不会导致线程安全问题。

  • 问题出在多个线程访问共享资源

      • 多个线程读共享资源其实也没有问题
      • 在多个线程对共享资源读写操作时发生指令交错,就会出现问题

一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区,其共享资源为临界资源

# 竞态条件

多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件

为了避免临界区的竞态条件发生,有多种手段可以达到目的:

  • 阻塞式的解决方案:synchronized,Lock
  • 非阻塞式的解决方案:原子变量cas

注意:

虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:

互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码

同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点

# volatile内存语义

# 作用

  • volatile是Java虚拟机提供的轻量级的同步机制(也算是可见性吧)
  • 保证被volatile修饰的共享变量对所有线程总数可见的,也就是当一个线程修改 了一个被volatile修饰共享变量的值,新值总是可以被其他线程立即得知。
  • 禁止指令重排序优化。

# volatile的可见性

# volatile无法保证原子性

# 指令重排序

为了充分利用CPU,会通过流水线将指令并行进行。为了能并行执行,又需要将指令进行重排序以便进行并行执行,以确保时间局部性原理,来利用cpu缓存。

# volatile禁止重排优化

这也是单例双重检测必须加他的原因,不加他可能会出现获得的对象属性是空的问题...

# 内存语义的实现

  • volatile写之前的操作不会被编译器重排序到volatile写之后
  • volatile读之后的操作不会被编译器重排序到volatile读之前。

# 硬件层的内存屏障

  • 保证特定操作的执行顺序
  • 保证某些变量的内存可见性
  • Memory Barrier的另外一个作用 是强制刷出各种CPU的缓存数据

# 参考资料

java内存屏障的原理与应用 (opens new window)

volatile实现内存可见性分析:字节码版本 (opens new window)

字节码: putstatic

volatile编译后JVM 会加上ACC_VOLATILE标识 putstatic字节码指令的解释器里面会使用C++的方法自带了编译器屏障的功能,总能拿到内存中的最新值。 在JVM 种通过acquire()方法实现内存屏障,最终还是反应到了内存屏障

关于java单例中使用使用volatile写双重检验锁的一点疑问? (opens new window)

instance = new Singleton()不是原子操作,这段代码可以简单分为下面三步执行:

  1. 为 instance 分配内存空间;

  2. 初始化 instance;

  3. 将 instance 指向分配的内存地址

由于但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 a 执行了 1 和 3,此时 线程 b 调用 getSingleton() 后发现 instance 不为空,因此返回 instance,但此时 instance 还未被初始化,所以就会导致空指针异常。

编辑 (opens new window)
上次更新: 2023/01/24, 15:21:15
计算机基础
java线程

← 计算机基础 java线程→

最近更新
01
架构升级踩坑之路
02-27
02
总结
02-27
03
语法学习
02-27
更多文章>
| Copyright © 2021-2025 Wu lingui |
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式