并发编程
2021-08-30 22:12:55 1 举报
登录查看完整内容
java并发编程JVM、JMM基础知识点
作者其他创作
大纲/内容
开始
对一个final对象的成员域的写和构造函数构造出的对象引用赋值给引用变量的顺序被禁止重排序。注:是在写final域的基础上增加了此条规则。
内存屏障
as-if-serial(单线程)(类似串行)
实例对象
禁止某些重排序?内存屏障
LoadLoad Barries
StoreLoad Barries
加锁流程
read: 在主内存读取一个变量,便于线程load到工作内存中
锁住
原因
消息传递
共享内存
进入同步块之前先在栈帧创建存储锁记录的空间,并将对象头的Mark Word复制到锁记录中,官方称为Displaced Mark Word
普通读
添加版本号即可解决,典型例子:数据库中的乐观锁;atomic包中的AtomicStampedReference
LoadLoad
目的是让一个CPU处理单元中的内存状态对其他CPU处理单元可见的一项技术
解决方案
普通写
final
重排序
CAS
缺点:线程阻塞,响应时间缓慢
静态域
类的实例对象
实例方法
寄存器
DCL问题(Double Check Lock)
volatile读
定义:如果两个操作同时访问同一个变量,且这两个操作至少存在一个写操作,此时这两个操作就存在数据依赖性
静态方法
2、让其他处理器对这个变量的缓存失效(多处理下,处理器会实现缓存一致性协议,没给处理器通过嗅探在总线上传播的数据来检查自己的缓存是不是过期了)
N
线程尝试使用CAS将Displaced Mark Word替换回到对象头
类对象
偏向锁
内存系统重排序
LoadStore Barries
暂停拥有偏向锁的线程
缓冲区
Happens-before规则
Y
全局安全点(当前没有字节码在运行)
尝试使用CAS将对象头的偏向锁指向自己
成功
写final域会要求编译器在写final域操作之后,构造函数返回之前插入StoreStore屏障;读final域的重排序规则会要求编译器在读final域之前插入一个LoadLoad屏障。
CAS的问题
指令:Load1:LoadLoad:Load2
finalize规则:一个对象的初始化完成(构造函数)先于它的finalize()方法的开始。
as-if-serial 和 happens-before
线程安全
注:是在写final域的基础上增加了此条规则。
write: 在主内存中操作,把store操作得到的变量值放入主内存的变量中
获取锁
指令:Store1:StoreLoad:Load2
程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作
配置的实例对象
指令:Load1:LoadStore:Store2
在一个线程中,初次读对象引用和初次读该对象包含的final域,JMM会禁止这两个操作的重排序,具体做法是在读这个final域的前面插入LoadLoad屏障
定义2:两个操作存在happens-before关系,并不意味着JVM平台的具体实现必须按照happens-before关系指定的顺序执行,如果重排序执行之后的结果和happens-before执行的结果一致,那么JMM允许这种重排序
StoreStore
PAUSE指令是指线程在自旋等待获取锁时,让cpu睡眠30个(about)clock,从而使得读操作的频率低很多
结束
join()规则:如果线程A执行了ThreadB.join()方法b并成功返回,那么线程B中的任意操作都happens-before 线程A中执行ThreadB.join()操作
线程之间的通信
StoreStore Barries
方法
四种内存屏障
读final域重排序规则
volatile写
将多个变量封装成一个对象,然后将这个对象做CAS就可以保证其原子性。atomic中的AtomicReference就可以用来保证对象之间的原子性
load: 在工作内存中把read读取到的变量值放入工作内存中的变量副本
对象头设置无锁状态
1、Lock前缀指令(汇编)将工作内存的值回写到主内存
编译器会在final域写之后,构造函数return之前,插入StoreStore屏障,这个屏障可以禁止处理器把final域重排序在构造函数之外
现代处理器采用了指令集并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序
处理器重排序
状态(只能升级不能降级)
原理
恢复暂停的线程
轻量级锁
as-if-serial语义给编写单线程程序的程序员创造了一个环境:单线程程序是按程序的顺序执行的; happens-before关系给编写正确同步的多线程程序的程序员创造了一个环境:正确同步的多线程程序是按照happens-before指定的顺序来执行的
缺点:线程竞争锁自旋会消耗CPU资源
无锁状态
底层使用处理器提供CMPXCHG指令(JDK1.5后)
第二项操作
为什么要延迟:因为如果不延迟,JVM在启动的时候出现锁竞争的话,锁就会从偏向锁升级为轻量级锁或者重量级锁
哪些是共享变量?
共享变量可以使用volatile保证线程间的变量同步
由于处理器使用缓存和读写缓冲区,使得加载和存储操作看上去可能是乱序执行的
失败(自旋)
1、在每个volatile写的前面插入一个StoreStore屏障
主要是线程的工作内存和主内存的数据不一致导致的
不存在数据依赖性的操作,在单线程下不一定是按照代码顺序执行的
as-if-serial语义保证单线程内和程序执行的结果不会被改变; happens-before关系保证正确同步的多线程程序的执行结果不会被改变
不管怎么重排序,程序的执行结果不能被改变
线程之间的同步
线程逃逸
StoreLoad
重量级锁
实现原理
修饰引用类型
具体规则
监视器锁规则(synchronized):锁的解锁,happens-before于随后对这个锁的加锁
as-if-serial 和 happens-before这样做的目的,都是为了在不改变执行结果的情况下,尽可能的提高程序执行的并发度。
对final修饰的对象的成员域写操作
volatile规则:对一个volatile的写,happens-before于任意后续对这个volatile域的读
锁标记位
只能保证一个共享变量的原子操作
volatile内存语义的实现
对final修饰的对象的成员域读操作
编译器重排序
4、在每个volatile读的后面插入一个LoadStore屏障
notify()
unlock: 在主内存操作,把一个被锁定的变量释放出来,其他线程可以进行锁定
失败(自旋获取锁)
优点:线程竞争不会阻塞,提高了程序的响应速度
数组元素
volatile
使用
Mark Word是否设置偏向锁标志的值为1?
synchronized优化
在java中,所有实例域、静态域和数组元素都放在堆内存中,所有线程都可以访问到。而局部变量、方法定义参数和异常处理器不会在线程之间共享,而共享数据会出现线程安全,非共享数据就不会出现线程安全的问题
获取偏向锁
假设所有线程访问共享资源都不会冲突,既然不会出现冲突那么就不会阻塞其它线程的操作。如果出现冲突,那么就重试(自旋)当前操作直到没有冲突
当一个线程执行对象的同步代码块或者同步方法时,需要获取对象的Monitor,才能执行,否则会被放到一个SynchronizedQueue中,并且线程状态变成BLOCKED,一直等待占有者释放Monitor,再尝试去获取对象的Monitor
是JMM对编译器和处理器重排序的约束原则。只要不改变程序的执行结果,编译器和处理器如何优化都行;因为程序员对于这两个操作是否被重排并不关心,程序员关心的是执行的结果会不会因为优化重排而被改变
LoadStore
JMM(java内存模型)
失败(表示当前锁存在竞争,此时锁会膨胀,升级为重量级锁)
在JDK1.5及以前,synchronized在处理线程竞争锁的时候会出现线程阻塞和唤醒带来的性能问题,阻塞后唤醒是比较耗时的操作;而在JDK1.5及以后,当线程没获取锁时,会进行一定的尝试,即非阻塞同步
指令:Store1:StoreStore:Store2
修饰基本类型
分代年龄
该线程是否存活
与happens-before关系
class对象
定义1:如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的顺序排在第二个操作之前
代码块
Java对象头
获取锁流程(对象头和栈帧中的锁记录里存储锁偏向的线程id)
JMM抽象结构模型
数据依赖性
指令并行重排序
wait()
Hashcode
线程尝试使用CAS将对象头的Mark Word指针指向锁记录的指针
3、在每个volatile读的后面插入一个LoadLoad屏障
3、处理器发现缓存失效后,会从主内存中重新获取变量的最新值
使用CAS竞争锁,此时的锁已经是轻量级锁
线程中断规则(interrupt):对线程interrupted()方法的调用先行于被中断线程的代码检测到中断时间的发生
volatile规则:对一个volatile的写,happens-before与任意后续对这个volatile域的读
synchronized
Mark Word偏向其他线程或者恢复到无锁或者标记该对象不适合作为偏向锁
实例域
编译器和处理器在处理重排序时,会遵守数据依赖性,编译器和处理器不会改变已经存在数据依赖性的两个操作的顺序
synchronized的happens-before关系
start()规则:如果线程A执行了ThreadB.start()方法(启动了线程B),那么线程A执行ThreadB.start()及之前的代码happens-beofre 线程B中的任意操作
ABA问题
任意实例对象object
对象锁(monitor)机制原理
目的:被volatile修饰的变量能够保证每个线程都能够获取到该变量的最新值,从而避免出现数据脏读的现象
编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序
JMM禁止编译器初始化final域被重排序在构造函数之外
自旋时间过长?如果JVM支持处理器的pause指令效率的话会有一定的提升
缓存
目的:在读一个对象的final域之前,一定会先读到这个包含final域的对象的引用
优点:加锁和解锁速度非常快,和执行非同步代码块只有纳秒及的差距
优点:线程竞争锁不会自旋,不会消耗CPU资源
本地内存
线程将主内存中的变量拷贝一个副本进行计算,计算完成之后将值写入主内存。但是共享变量可能会出现线程安全问题,可以对变量使用volatile关键字保证变量对每个线程可见,并且在单个线程写入完成后能够强制刷新到主内存
判断对象头的Mark Word里是否存储着指向当前线程id的偏向锁
assign: 在工作内存中操作,把一个从执行引擎收到的值赋值给工作内存中的变量,每当虚拟机遇到一个变量赋值的操作字节码指令时就会执行这个操作
禁止特定类型的编译器重排序
JMM的保守策略
lock: 在主内存操作,标识变量是独占模式
store: 在工作内存中操作,把一个工作内存中的值传递给主内存中,便于后续的write操作使用
use: 在工作内存中操作,把变量值传递给执行引擎,每当虚拟机遇到一个需要使用到这个变量的字节码指令时将会执行这个操作
缺点:如果线程之间存在竞争,会带来额外的锁撤销的消耗
原子性
释放锁流程(等到竞争的时候,持有偏向锁的线程才会释放锁)
第一项操作
2、在每个volatile写的后面插入一个StoreLoad屏障
解锁流程
主内存
写final域重排序规则
Hotspot的作者研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获取,为了让锁的获取成本降低而引入了偏向锁
重排序规则表
0 条评论
回复 删除
下一页