请选择 进入手机版 | 继续访问电脑版

简体中文 繁體中文 English 日本語 Deutsch 한국 사람 بالعربية TÜRKÇE português คนไทย

彻底理解Java内存模型,它为什么会引发线程安全问题

[复制链接]

彻底理解Java内存模型,它为什么会引发线程安全问题

发表于 2022-1-18 15:15:22 只看大图 阅读模式 倒序浏览
3848 0 查看全部
  近日有热心市民就 "Java内存模型 " 提出质疑 线程是否会把所有需要操作的数据全加载到内存
cc236cba777a4c90ae5becdbc2fd31b6.png
根据《我是憨包》可以看出当事人蛋蛋(化名)目前情绪稳定并且似乎已经意识到问题所在
  是的聪明的蛋蛋已经找到了答案答案后面再说
  此事件发生后群内大佬高度重视立即召开线上会议成立Java内存模型专家小组作出响应要求组织迅速妥善处理迅速查清问题根源立即组织开展在线答疑进一步做好指导工作防止同样问题再次出现阻挠兄弟们拿到心仪offer
  一想到很多朋友还没搞懂Java内存模型我就饭吃不饱觉睡不着就连看到黑丝也无动于衷
  于是
  又花了几天时间 又花了几根头发来尝试帮大家理解一波~
  关于Java内存模型能扯好多好多、能聊好远好远但是不要慌我们整理下问题先
  • 什么是Java内存模型
  • 为什么会有Java内存模型
  • Java内存模型引发了什么问题
  • 线程是否会把所有需要操作的数据全加载到内存
  据当事人陈述

  1. 线程在操作数据时会从主内存中拷贝一份数据副本到自己的工作内存操作完再写回主内存那如果这个数据超级大也会拷贝到工作内存中吗
复制代码
  要想弄清这个问题我们必须先研究下什么是Java内存模型
  很多同学会把 Java内存模型 和 JVM内存模型 搞混这是两种截然不同的东西
  Java内存模型全称Java Memory Model简称JMM是一种虚拟机规范下面会详细讲
JVM内存模型全称Java Virtual Machine简称JVM也是一种虚拟机规范关于jvm本文不会展开讲
  如果想开发一款能运行Java程序的虚拟机就必须遵循这两个规范当然需要遵循的规范远不止这两种只有这样java程序才能在你的虚拟机上开开心心的run我们最最最最常见的hotspot vm就遵循了这些规范
Java内存模型的由来
72cc8f1b7b224e2cb551723ac60a0ac6.png
  说来话长
edac60a1ed7849bb846aa5836424db4a.png
我长话短说吧
问题起源  这牵涉到CPU厂商和内存厂商的发展史。。。
  我们鸡道cpu在执行指令的时候经常需要操作内存中的数据
  为了方便理解我举个栗子拿 i = i + 1来讲
  cpu先要从内存中读取到 i 当前的值进行 +1 再将计算结果写回内存
watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA6LSf5YC656iL5bqP54y_,size_13,color_FFFFFF,t_70,g_se,x_16.jpg
  最开始一切安好但随着技术的发展cpu执行效率远远超过了内存的读写效率所以出现了一个现象

  1. cpu执行 +1 操作耗时很短假设只需要1ms而从内存中读取 i 再写回内存耗时很长假设是10ms
复制代码
  cpu明明只需要1ms活生生被内存拖到11ms这哪儿顶得住啊
  于是机智的cpu厂想了个办法
解决办法  这个办法在《深入理解Java虚拟机》书中也有提到
watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA6LSf5YC656iL5bqP54y_,size_20,color_FFFFFF,t_70,g_se,x_16.jpg
  简单来说就在cpu和内存中间加一层 高速缓存也就是我们平时说的L1、L2、L3缓存这一块缓存一般比较小但嗷嗷快你懂我意思吧
   注意知识点来了一定要把cpu的高速缓存和内存条的内存区分开
  这是内存条的内存系统属性中可以查看
e232ab47267143ada815583519b049d0.png
  这是cpu的高速缓存任务管理器-性能一栏可以查看到
9f0f102a49fe4d6fa2df33bdb1b13624.png
  所以现在操作流程变成了

  1. cpu会事先将需要用到的数据从主内存中复制一份到高速缓存cpu在执行计算操作时依次从L1、L2、L3级缓存中查找如果有需要的数据直接操作计算结束后再flush到主内存中如果没有再去主内存中查找
复制代码
watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA6LSf5YC656iL5bqP54y_,size_20,color_FFFFFF,t_70,g_se,x_16.jpg
  cpu被内存拉低效率的问题得以解决
  时间又过了很久。。。
  cpu厂商推出了多核处理器又引出了另一个问题 线程安全
  多核处理器的每个核心都有自己的高速缓存每个cpu架构都不同要具体看cpu厂商怎么做目前市面上的cpu一般都是L1、L2独立L3共享
  上面可以看到我cpu的L1缓存是384k这384k并不是六个核共享而是 6 * 32 * 2如下图
1e655cb6c56f46138336e07963f06797.png
  现在架构变成了
watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA6LSf5YC656iL5bqP54y_,size_20,color_FFFFFF,t_70,g_se,x_16.jpg
这个图是简化版实际的架构图比这复杂得多那些细节我懒的画了
  所以现在问题来了如果不同核心上的线程同时操作同一个数据会出现什么问题
  我们假设一下
   核心a有个线程t1核心b有个线程t2
开始计算前内存中 i 的值是0两线程对应高速缓存中 i 的值也都是0
某一时刻两线程同时执行 i + 1
t1执行完 i = 1吭呲吭呲写回内存此时内存中 i 的值已经由0变为了1
t2执行完 i 也 = 1也吭呲吭呲把i = 1写回内存这就把t1写回的新 i 值覆盖了

   本来 i 经过两次+1应该等于2实际结果却等于1懂我意思吗大多数并发编程中的数据异常问题都是这么来的
  所以并发编程中只要涉及到写的操作我们都应该保证同步从而得到可靠的最终数据
  到这里我们可以总结下什么内存模型
什么是Java内存模型  由上面的架构图可见线程需要

  1. 上面说了Java内存模型就是一种协议线程要操作数据需要先从主内存中读取到工作内存操作完再写回主内存看起来简单但这之间有很多底层技术细节比如 什么时候读取 又什么时候写入 多个线程共同读写时又该如何调配 所以问题来了一台服务器上的cpu和内存可能是由不同厂商提供的如果它们的底层实现细节对不上那怎么保证程序能够正常运行不可能每次设计产品时都把所有厂商拉一起开个会吧所以为了方便为了统一有了Java内存模型它被用来 规范不同硬件和操作系统在内存读写底层实现上的差异 只有屏蔽这些差异Java才能实现 一次编译、处处运行
复制代码
  又回到最初的起点、记忆中你青涩的脸~
现在公布答案
watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA6LSf5YC656iL5bqP54y_,size_10,color_FFFFFF,t_70,g_se,x_16.jpg
  说到这儿再扯一嘴cpu更底层的冷知识
指令重排  并发编程中除了Java内存模型带来的线程安全问题cpu、虚拟机自身也存在类似问题
  • 关于cpu为了从分利用cpu实际执行指令时会做优化
  • 关于虚拟机在HotSpot vm中为了提升执行效率JIT(即时编译)模式也会做指令优化
  指令重排在大部分场景下确实能提升效率但有些操作对代码执行顺序是强依赖的此时我们需要关闭指令重排相信很多朋友已经猜到了
  没错就是volatile
  关于volatile想要彻底理解也得扯很多很多此处就不扯了改天单独写一篇
  举个例来说明什么指令重排及如何防范
watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA6LSf5YC656iL5bqP54y_,size_16,color_FFFFFF,t_70,g_se,x_16.jpg
这个伪代码取自《深入理解Java虚拟机》
其中描述的场景是开发中常见配置读取过程只是我们在处理配置文件时一般不会出现并发所以没有察觉这会有问题。
试想一下如果定义initialized变量时没有使用volatile修饰就可能会由于指令重排序的优化导致位于线程A中最后一条代码“initialized=true”被提前执行这里虽然使用Java作为伪代码但所指的重排序优化是机器级的优化操作提前执行是指这条语句对应的汇编代码被提前执行这样在线程B中使用配置信息的代码就可能出现错误而volatile关键字则可以避免此类情况的发生

  ok我话说完

来源:CSDN
回复

使用道具 举报

游客~
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Archiver|手机版|小黑屋|极客同行 ( 蜀ICP备17009389号-1 ) 川公网安备 51019002006459号

© 2013-2016 Comsenz Inc. Powered by Discuz! X3.4