手牵手心连心 發表於 2025-9-14 16:11:00

Java并发编程(3)

<hr>
<h1 style="text-align: center">Java内存模型</h1>
<h1>1、说一下你对Java内存模型(JMM)的理解</h1>
<p>&nbsp;  Java程序运行在各种硬件和操作系统上,不同硬件的CPU缓存策略、内存访问顺序、指令重排规则可能都不一样。那JMM是Java规范定义的一个抽象模型,是一套规则:</p>
<ul>
<li><strong>线程和主内存的交互</strong>:线程如何从主内存读变量、写变量</li>
<li><strong>可见性保证</strong>:什么时候一个线程对变量的修改能被另一个线程看到</li>
<li><strong>有序性保证</strong>:哪些操作在多线程下不能随意重排,哪些可以。</li>
</ul>
<div class="cnblogs_Highlighter">
<pre class="brush:java;gutter:true;">//例如
volatile int flag = 0
//在x86CPU上可能会翻译成某种内存屏障指令
//在ARM CPU上可能是另一种
//但Java程序员只需知道:volatile保证可见性和禁止指令重排,效果是一样的
</pre>
</div>
<p>  JMM定义了线程和主内存之间的抽象关系:线程之间共享变量存储在主内存,每个线程有一个私有的本地内存。</p>
<p>  如果是双核CPU架构:</p>
<ul>
<li>每个核心:控制器+运算器+私有的一级缓存(L1缓存)</li>
<li>共享缓存:有个架构有L2或L3,多个核心共享</li>
<li>主内存:所有CPU都能访问</li>
</ul>
<p>  JMM内存模型里定义了两个层次:</p>
<ul>
<li><strong>主内存</strong>:所有线程共享,对应硬件上的主内存(DRAM)</li>
<li><strong>工作内存</strong>:每个线程独有,用来保存主内存中变量的副本</li>
<li>流程:
<ul>
<li>变量先从住内存加载到工作内存(寄存器/缓存)</li>
<li>线程所有操作只在工作内存完成</li>
<li>结果再写回主内存</li>
<li>线程之间想看到对方的修改,必须通过<strong>主内存</strong>完成数据交换</li>
</ul>
</li>
</ul>
<p>&nbsp;</p>
<h1>2、说说你对原子性、可见性、有序性的理解</h1>
<ul>
<li><strong>&nbsp;原子性</strong>:一个操作不可再分,要么全部完成,要么全部不做
<ul>
<li>在Java中,基本的读取和写入(如int x = 1)是原子的。但复合操作不是原子的(如i++)</li>
<li><strong>保证方式</strong>:synchronized或ReetrantLock(锁住临界区)、AtomicInteger、AtomicLong等原子类(通过CAS+volatile)</li>
</ul>
</li>
<li><strong>可见性</strong>:一个线程对共享变量的修改,能被其他线程及时看到。(由于CPU缓存和寄存器存在,线程可能看到的是旧值)
<ul>
<li>保证方式:<strong>volatile</strong>(保证写入立刻刷新到主内存);synchronized/Lock(解锁时强制刷新到主内存,加锁时清空工作内存,重新读)</li>
</ul>
</li>
<li><strong>有序性</strong>:程序执行顺序和代码顺序一致,但编译器和CPU为了优化,可能会指令重排。(单线程不影响,多线程可能影响)
<ul>
<li>保证方式:<strong>volatile</strong>禁止指令重排;<strong>synchronized/Lock</strong>进入临界区和退出时,JMM会插入内存屏障,保证临界区内操作的顺序性。JMM的happens-before原则,定义哪些操作必须对另一个操作可见,从而间接约束了顺序。</li>
</ul>
</li>
</ul>
<h1>3、说说什么是指令重排</h1>
<p>&nbsp;  指令重排 = 编译器或CPU在执行时,为了优化性能,会调整代码语句的执行顺序。(有序性)</p>
<p>  三种指令重排类型:</p>
<ul>
<li>(1)<strong>编译器优化的重排</strong>:
<ul>
<li>Java源代码--&gt;字节码--&gt;机器指令,中间编译器可能优化。只要不改变单线程的最终结果,就可以调整语句顺序。</li>
</ul>
</li>
(2)指令级并行(ILP)重排
<ul>
<li>CPU支持流水行并行,若指令间没有数据依赖,CPU会乱序执行以提高效率</li>
</ul>
<li>(3)<strong>内存系统的重排</strong>
<ul>
<li>因为有CPU cache,写缓冲区,导致内存的读写顺序看起来是乱的。</li>
<li>假如线程A对变量x写入后,先放在写缓冲区,没立刻刷新到主内存。线程B去读时,可能还是旧值。</li>
</ul>
</li>
</ul>
<div class="cnblogs_Highlighter">
<pre class="brush:java;gutter:true;">instance = new Singleton();
</pre>
</div>
<p>  三个底层步骤(理想顺序):</p>
<ul>
<li>分配内存:给Singleton对象分配一块内存控件,假设内存地址时0x1234。</li>
<li>调用构造方法:在0x1234这块内存上,执行构造函数,把对象真正初始化好(比如成员变量赋值)</li>
<li>把引用赋给变量instance:instance指向0x1234,之后通过instance就能找到这个对象。</li>
</ul>
<p>  指令重排(为了优化性能,步骤2和3可能被交换)。若第三步变成第二步,此时对象还没初始化完。</p>
<p>  如果是多线程:A先执行new Singleton(),到第二步引用赋值给instance,此时线程A被切换走了。线程B看到if(instance == null),发现instance不是null,就直接返回instance,但其实这个uidx还没初始化完成。就可能会出现“半初始化对象”被使用的情况。</p>
<h1>4、指令重排有限制吗?happens-before了解吗</h1>
<p>&nbsp;  是有限制的,需要遵守两个主要约束:<strong>as-if-serial(后面讲)</strong>和<strong>happens-before</strong>规则。</p>
<p>  happens-before规则是JMM提供的多线程间的有序性保证,定义了哪些操作对其他线程可见、必须按顺序。定义:如果操作A&nbsp;&nbsp;happens-before 操作B,那A的结果必须对B可见,且A的执行顺序排在B之前。(注意,这是一种约束关系,不等于物理时间顺序。这只是用来保证逻辑先后关系,用来保证多线程下结果正确,同时允许底层做性能优化)</p>
<p>  六大原则:</p>
<ul>
<li><strong>程序顺序规则</strong>:在一个线程内,按代码顺序,前面的操作happens-before 后面的操作</li>
<li><strong>监视器锁规则</strong>:对一个锁的解锁&nbsp;happens-before 随后对这个锁的解锁。(如线程A释放锁-&gt;线程B获取同一把锁--&gt;B必然能看到A的修改)</li>
<li><strong>volatile变量规则:</strong>对一个volatile变量的写&nbsp;happens-before 后续对这个变量的读。(如线程A flag = true--&gt;线程B读取flag一定能看到true)</li>
<li><strong>传递性</strong>:若A&nbsp;happens-before B,B&nbsp;happens-before&nbsp; C,那么A&nbsp;happens-before&nbsp; C。</li>
<li><strong>start规则</strong>:线程A调用threadB.start(),happens-before 线程B的任意操作。(如A在启动B之前的写操作,B一定都能看到)</li>
<li><strong>join()规则</strong>:线程A调用threadB.join()并成功返回,意味着线程B的所有操作happens-before A从join返回(如B执行完写操作,A在join后一定能看到结果)</li>
</ul>
<h1>5、as-if-serial是什么?单线程的程序一定是顺序的吗?</h1>
<p>&nbsp;  as-if-serial意思是:不管怎么重排,单线程程序的执行结果不能被改变。编译器和处理器不会对存在数据依赖关系的操作做重排,因为这会改变执行结果。但是,若操作之间不存在数据依赖关系,这些操作可能会被编译器和处理器重排。</p>
<div class="cnblogs_Highlighter">
<pre class="brush:java;gutter:true;">double p i = 3.14 ; // A
double r = 1.0 ; // B
double area = p i * r * r ; // C
//C依赖A和B,A和B之间没有依赖
//顺序1:A-B-C
//顺序2:B-A-C
//C不可能在A、B前面</pre>
</div>
<h1>6、volatile实现原理</h1>
<p>&nbsp;(1)可见性</p>
<p>  相比synchronized的加锁方式来解决共享变量的内存可见性问题,volatile是更轻量的选择,没有上下文切换的额外开销成本。一个变量被声明为volatile时,线程再写入变量时不会把值缓存在寄存器或其他地方,而是会把值刷新回主内存,当其他线程读取该共享变量,会从主内存获取最新值,而不是使用当前线程的本地内存中的值。</p>
<p><img src="https://img2024.cnblogs.com/blog/2297173/202509/2297173-20250914155048975-993860655.png"></p>
<p>&nbsp;(2)有序性</p>
<p>  没有内存屏障可能会发生什么?</p>
<ul>
<li>CPU可能把flag=true先执行并刷出,而a=1还在寄存器/缓存里没同步到主内存。</li>
<li>指令乱序,导致“半初始化对象”</li>
<li>读到旧值(缓存不一致),若没有屏障,写操作不会强制刷新到主内存</li>
</ul>
<p>  volatile怎么保证有序性:JMM在volatile前后都会插入内存屏障,限制重排。</p>
<ul>
<li>写volatile前:保证之前写的变量先对外可见;保证bolatile写对后续读可见</li>
<li>读volatile时:保证volatile读完后,才能读其他变量;保证volatile读完后,才能写其他变量。</li>
</ul>
<p>  volatile修饰:实例变量、静态变量。不能修饰局部变量、方法和类(在线程栈中,本来就不共享)</p>
<blockquote>
<p>线程安全:保证原子性、可见性、有序性</p>
<p>volatile只能保证后两者。</p>
</blockquote>
<p>&nbsp;</p>
<h1>参考</h1>
<p> 沉默王二公众号</p><br><br>
来源:https://www.cnblogs.com/xiaoqian01/p/19091301
頁: [1]
查看完整版本: Java并发编程(3)