首页 > 基础资料 博客日记
Java并发编程(3)
2025-09-14 16:30:01基础资料围观9次
Java内存模型
1、说一下你对Java内存模型(JMM)的理解
Java程序运行在各种硬件和操作系统上,不同硬件的CPU缓存策略、内存访问顺序、指令重排规则可能都不一样。那JMM是Java规范定义的一个抽象模型,是一套规则:
- 线程和主内存的交互:线程如何从主内存读变量、写变量
- 可见性保证:什么时候一个线程对变量的修改能被另一个线程看到
- 有序性保证:哪些操作在多线程下不能随意重排,哪些可以。
//例如 volatile int flag = 0 //在x86CPU上可能会翻译成某种内存屏障指令 //在ARM CPU上可能是另一种 //但Java程序员只需知道:volatile保证可见性和禁止指令重排,效果是一样的
JMM定义了线程和主内存之间的抽象关系:线程之间共享变量存储在主内存,每个线程有一个私有的本地内存。
如果是双核CPU架构:
- 每个核心:控制器+运算器+私有的一级缓存(L1缓存)
- 共享缓存:有个架构有L2或L3,多个核心共享
- 主内存:所有CPU都能访问
JMM内存模型里定义了两个层次:
- 主内存:所有线程共享,对应硬件上的主内存(DRAM)
- 工作内存:每个线程独有,用来保存主内存中变量的副本
- 流程:
- 变量先从住内存加载到工作内存(寄存器/缓存)
- 线程所有操作只在工作内存完成
- 结果再写回主内存
- 线程之间想看到对方的修改,必须通过主内存完成数据交换
2、说说你对原子性、可见性、有序性的理解
- 原子性:一个操作不可再分,要么全部完成,要么全部不做
- 在Java中,基本的读取和写入(如int x = 1)是原子的。但复合操作不是原子的(如i++)
- 保证方式:synchronized或ReetrantLock(锁住临界区)、AtomicInteger、AtomicLong等原子类(通过CAS+volatile)
- 可见性:一个线程对共享变量的修改,能被其他线程及时看到。(由于CPU缓存和寄存器存在,线程可能看到的是旧值)
- 保证方式:volatile(保证写入立刻刷新到主内存);synchronized/Lock(解锁时强制刷新到主内存,加锁时清空工作内存,重新读)
- 有序性:程序执行顺序和代码顺序一致,但编译器和CPU为了优化,可能会指令重排。(单线程不影响,多线程可能影响)
- 保证方式:volatile禁止指令重排;synchronized/Lock进入临界区和退出时,JMM会插入内存屏障,保证临界区内操作的顺序性。JMM的happens-before原则,定义哪些操作必须对另一个操作可见,从而间接约束了顺序。
3、说说什么是指令重排
指令重排 = 编译器或CPU在执行时,为了优化性能,会调整代码语句的执行顺序。(有序性)
三种指令重排类型:
- (1)编译器优化的重排:
- Java源代码-->字节码-->机器指令,中间编译器可能优化。只要不改变单线程的最终结果,就可以调整语句顺序。
(2)指令级并行(ILP)重排
- CPU支持流水行并行,若指令间没有数据依赖,CPU会乱序执行以提高效率
- (3)内存系统的重排
- 因为有CPU cache,写缓冲区,导致内存的读写顺序看起来是乱的。
- 假如线程A对变量x写入后,先放在写缓冲区,没立刻刷新到主内存。线程B去读时,可能还是旧值。
instance = new Singleton();
三个底层步骤(理想顺序):
- 分配内存:给Singleton对象分配一块内存控件,假设内存地址时0x1234。
- 调用构造方法:在0x1234这块内存上,执行构造函数,把对象真正初始化好(比如成员变量赋值)
- 把引用赋给变量instance:instance指向0x1234,之后通过instance就能找到这个对象。
指令重排(为了优化性能,步骤2和3可能被交换)。若第三步变成第二步,此时对象还没初始化完。
如果是多线程:A先执行new Singleton(),到第二步引用赋值给instance,此时线程A被切换走了。线程B看到if(instance == null),发现instance不是null,就直接返回instance,但其实这个uidx还没初始化完成。就可能会出现“半初始化对象”被使用的情况。
4、指令重排有限制吗?happens-before了解吗
是有限制的,需要遵守两个主要约束:as-if-serial(后面讲)和happens-before规则。
happens-before规则是JMM提供的多线程间的有序性保证,定义了哪些操作对其他线程可见、必须按顺序。定义:如果操作A happens-before 操作B,那A的结果必须对B可见,且A的执行顺序排在B之前。(注意,这是一种约束关系,不等于物理时间顺序。这只是用来保证逻辑先后关系,用来保证多线程下结果正确,同时允许底层做性能优化)
六大原则:
- 程序顺序规则:在一个线程内,按代码顺序,前面的操作happens-before 后面的操作
- 监视器锁规则:对一个锁的解锁 happens-before 随后对这个锁的解锁。(如线程A释放锁->线程B获取同一把锁-->B必然能看到A的修改)
- volatile变量规则:对一个volatile变量的写 happens-before 后续对这个变量的读。(如线程A flag = true-->线程B读取flag一定能看到true)
- 传递性:若A happens-before B,B happens-before C,那么A happens-before C。
- start规则:线程A调用threadB.start(),happens-before 线程B的任意操作。(如A在启动B之前的写操作,B一定都能看到)
- join()规则:线程A调用threadB.join()并成功返回,意味着线程B的所有操作happens-before A从join返回(如B执行完写操作,A在join后一定能看到结果)
5、as-if-serial是什么?单线程的程序一定是顺序的吗?
as-if-serial意思是:不管怎么重排,单线程程序的执行结果不能被改变。编译器和处理器不会对存在数据依赖关系的操作做重排,因为这会改变执行结果。但是,若操作之间不存在数据依赖关系,这些操作可能会被编译器和处理器重排。
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前面
6、volatile实现原理
(1)可见性
相比synchronized的加锁方式来解决共享变量的内存可见性问题,volatile是更轻量的选择,没有上下文切换的额外开销成本。一个变量被声明为volatile时,线程再写入变量时不会把值缓存在寄存器或其他地方,而是会把值刷新回主内存,当其他线程读取该共享变量,会从主内存获取最新值,而不是使用当前线程的本地内存中的值。
(2)有序性
没有内存屏障可能会发生什么?
- CPU可能把flag=true先执行并刷出,而a=1还在寄存器/缓存里没同步到主内存。
- 指令乱序,导致“半初始化对象”
- 读到旧值(缓存不一致),若没有屏障,写操作不会强制刷新到主内存
volatile怎么保证有序性:JMM在volatile前后都会插入内存屏障,限制重排。
- 写volatile前:保证之前写的变量先对外可见;保证bolatile写对后续读可见
- 读volatile时:保证volatile读完后,才能读其他变量;保证volatile读完后,才能写其他变量。
volatile修饰:实例变量、静态变量。不能修饰局部变量、方法和类(在线程栈中,本来就不共享)
线程安全:保证原子性、可见性、有序性
volatile只能保证后两者。
参考
[1] 沉默王二公众号
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:jacktools123@163.com进行投诉反馈,一经查实,立即删除!
标签:
上一篇:Java并发编程(1)
下一篇:没有了