【Java经典面试题系列】5.37道算法面试题

在这里插入图片描述

Q1:JMM的作⽤是什么?

Java 线程的通信由 JMM 控制,JMM 的主要⽬的是定义程序中各种变量的访问规则。变量包括实例字

段、静态字段,但不包括局部变量与⽅法参数,因为它们是线程私有的,不存在多线程竞争。JMM 遵循

⼀个基本原则:只要不改变程序执⾏结果,编译器和处理器怎么优化都⾏。例如编译器分析某个锁只会

单线程访问就消除锁,某个 volatile 变量只会单线程访问就把它当作普通变量。

JMM 规定所有变量都存储在主内存,每条线程有⾃⼰的⼯作内存,⼯作内存中保存被该线程使⽤的变量

的主内存副本,线程对变量的所有操作都必须在⼯作空间进⾏,不能直接读写主内存数据。不同线程间

⽆法直接访问对⽅⼯作内存中的变量,线程通信必须经过主内存。

关于主内存与⼯作内存的交互,即变量如何从主内存拷⻉到⼯作内存、从⼯作内存同步回主内存,JMM

定义了 8 种原⼦操作:

在这里插入图片描述

Q2:as-if-serial 是什么?

不管怎么重排序,单线程程序的执⾏结果不能改变,编译器和处理器必须遵循 as-if-serial 语义。

为了遵循 as-if-serial,编译器和处理器不会对存在数据依赖关系的操作重排序,因为这种重排序会改变

执⾏结果。但是如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。

as-if-serial 把单线程程序保护起来,给程序员⼀种幻觉:单线程程序是按程序的顺序执⾏的。

Q3:happens-before 是什么?

先⾏发⽣原则,JMM 定义的两项操作间的偏序关系,是判断数据是否存在竞争的重要⼿段。

JMM 将 happens-before 要求禁⽌的重排序按是否会改变程序执⾏结果分为两类。对于会改变结果的重

排序 JMM 要求编译器和处理器必须禁⽌,对于不会改变结果的重排序,JMM 不做要求。

JMM 存在⼀些天然的 happens-before 关系,⽆需任何同步器协助就已经存在。如果两个操作的关系不

在此列,并且⽆法从这些规则推导出来,它们就没有顺序性保障,虚拟机可以对它们随意进⾏重排序。

  • **程序次序规则:**⼀个线程内写在前⾯的操作先⾏发⽣于后⾯的。

  • 管程锁定规则: unlock 操作先⾏发⽣于后⾯对同⼀个锁的 lock 操作。

  • volatile **规则:**对 volatile 变量的写操作先⾏发⽣于后⾯的读操作。

  • **线程启动规则:**线程的 start ⽅法先⾏发⽣于线程的每个动作。

  • **线程终⽌规则:**线程中所有操作先⾏发⽣于对线程的终⽌检测。

  • **对象终结规则:**对象的初始化先⾏发⽣于 finalize ⽅法。

  • **传递性:**如果操作 A 先⾏发⽣于操作 B,操作 B 先⾏发⽣于操作 C,那么操作 A 先⾏发⽣于操作

C 。

Q4:as-if-serial happens-before 有什么区别?

as-if-serial 保证单线程程序的执⾏结果不变,happens-before 保证正确同步的多线程程序的执⾏结果

不变。

这两种语义的⽬的都是为了在不改变程序执⾏结果的前提下尽可能提⾼程序执⾏并⾏度。

Q5:什么是指令重排序?

为了提⾼性能,编译器和处理器通常会对指令进⾏重排序,重排序指从源代码到指令序列的重排序,分

为三种:① 编译器优化的重排序,编译器在不改变单线程程序语义的前提下可以重排语句的执⾏顺序。

② 指令级并⾏的重排序,如果不存在数据依赖性,处理器可以改变语句对应机器指令的执⾏顺序。③

内存系统的重排序。

Q6:原⼦性、可⻅性、有序性分别是什么?

原⼦性

基本数据类型的访问都具备原⼦性,例外就是 long 和 double,虚拟机将没有被 volatile 修饰的 64 位

数据操作划分为两次 32 位操作。

如果应⽤场景需要更⼤范围的原⼦性保证,JMM 还提供了 lock 和 unlock 操作满⾜需求,尽管 JVM 没

有把这两种操作直接开放给⽤户使⽤,但是提供了更⾼层次的字节码指令 monitorenter 和

monitorexit,这两个字节码指令反映到 Java 代码中就是 synchronized。

可⻅性

可⻅性指当⼀个线程修改了共享变量时,其他线程能够⽴即得知修改。JMM 通过在变量修改后将值同步

回主内存,在变量读取前从主内存刷新的⽅式实现可⻅性,⽆论普通变量还是 volatile 变量都是如此,

区别是 volatile 保证新值能⽴即同步到主内存以及每次使⽤前⽴即从主内存刷新。

除了 volatile 外,synchronized 和 fifinal 也可以保证可⻅性。同步块可⻅性由"对⼀个变量执⾏ unlock

前必须先把此变量同步回主内存,即先执⾏ store 和 write"这条规则获得。fifinal 的可⻅性指:被 fifinal

修饰的字段在构造⽅法中⼀旦初始化完成,并且构造⽅法没有把 this 引⽤传递出去,那么其他线程就能

看到 fifinal 字段的值。

有序性

有序性可以总结为:在本线程内观察所有操作是有序的,在⼀个线程内观察另⼀个线程,所有操作都是

⽆序的。前半句指 as-if-serial 语义,后半句指指令重排序和⼯作内存与主内存延迟现象。

Java 提供 volatile 和 synchronized 保证有序性,volatile 本身就包含禁⽌指令重排序的语义,⽽

synchronized 保证⼀个变量在同⼀时刻只允许⼀条线程对其进⾏ lock 操作,确保持有同⼀个锁的两个

同步块只能串⾏进⼊。

Q7:谈⼀谈volatile

JMM 为 volatile 定义了⼀些特殊访问规则,当变量被定义为 volatile 后具备两种特性:

  • 保证变量对所有线程可⻅当⼀条线程修改了变量值,新值对于其他线程来说是⽴即可以得知的。volatile 变量在各个线程的

⼯作内存中不存在⼀致性问题,但 Java 的运算操作符并⾮原⼦操作,导致 volatile 变量运算在并

发下仍不安全。

  • 禁⽌指令重排序优化

使⽤ volatile 变量进⾏写操作,汇编指令带有 lock 前缀,相当于⼀个内存屏障,后⾯的指令不能

重排到内存屏障之前。

使⽤ lock 前缀引发两件事:① 将当前处理器缓存⾏的数据写回系统内存。②使其他处理器的缓存

⽆效。相当于对缓存变量做了⼀次 store 和 write 操作,让 volatile 变量的修改对其他处理器⽴即

可⻅。

静态变量 i 执⾏多线程 i++ 的不安全问题

⾃增语句由 4 条字节码指令构成的,依次为 getstatic 、 iconst_1 、 iadd 、 putstatic ,当

getstatic 把 i 的值取到操作栈顶时,volatile 保证了 i 值在此刻正确,但在执⾏ iconst_1 、 iadd

时,其他线程可能已经改变了 i 值,操作栈顶的值就变成了过期数据,所以 putstatic 执⾏后就可能

把较⼩的 i 值同步回了主内存。

适⽤场景

① 运算结果并不依赖变量的当前值。② ⼀写多读,只有单⼀的线程修改变量值。

内存语义

写⼀个 volatile 变量时,把该线程⼯作内存中的值刷新到主内存。

读⼀个 volatile 变量时,把该线程⼯作内存值置为⽆效,从主内存读取。

指令重排序特点

第⼆个操作是 volatile 写,不管第⼀个操作是什么都不能重排序,确保写之前的操作不会被重排序到写

之后。

第⼀个操作是 volatile 读,不管第⼆个操作是什么都不能重排序,确保读之后的操作不会被重排序到读

之前。

第⼀个操作是 volatile 写,第⼆个操作是 volatile 读不能重排序。

JSR-133 增强 volatile 语义的原因

在旧的内存模型中,虽然不允许 volatile 变量间重排序,但允许 volatile 变量与普通变量重排序,可能

导致内存不可⻅问题。JSR-133 严格限制编译器和处理器对 volatile 变量与普通变量的重排序,确保

volatile 的写-读和锁的释放-获取具有相同的内存语义。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

凌风_Java高性能架构

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值