java并发机制的底层实现原理
重点的关键字、词汇
- cpu中的一些术语
-
volatile
- volatile可以看做是是轻量级的synchronized
- 在多处理器开发中保证了共享变量的“可见性” (可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。 )
- 如果一个字段被声明成volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的
- 有volatile变量修饰的共享变量进行写操作的时候会额外生成一个Lock前缀的指令
- 将当前处理器缓存行的数据写会到系统内存
- 这个写会内存的操作会是在其他CPU里缓存了该内存地址的数据无效。
- L1,L2是每个处理器核心所独有的,L3是共享的,都是高速缓存区
- 缓存一致性协议
- 每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里
- 缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据。 (缓存锁定)
- MESI协议
MESI(修改、独占、共享、无效)控制协议去维护内部缓存和其他处理器缓存的一致
性
- 使用优化
- java 7 中用一种追加字节的方式来优化队列出队和入队的性能,后边的可能不支持
-
synchronized 重量级锁
利用synchronized实现同步的基础-
Java中的每一个对象都可以作为锁 (必须记下来)
- 对于普通同步方法,锁是当前实例对象
- 对于静态同步方法,锁是当前类的Class对象。
- 对于同步方法块,锁是Synchonized括号里配置的对象
-
当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁
-
JVM基于进入和退出Monitor对象来实现方法同步和代码块同步
-
monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处
-
JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。
-
线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁
-
锁的信息存储在java对象的对象头的mark word中
-
四种锁 (锁的级别只能升高,不能降低)
- 无锁
- 偏向锁
偏向锁的初始化流程
偏向锁在Java 6和Java 7里是默认启用的,但是它在应用程序启动几秒钟之后才激活,如有必要可以使用JVM参数来关闭延迟:-XX:BiasedLockingStartupDelay=0。如果你确定应用程序里所有的锁通常情况下处于竞争状态,可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,那么程序默认会进入轻量级锁状态。
-
轻量级锁
- 自旋 while(true)
- 加锁
线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
- 解锁
轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。
-
重量级锁
-
锁升级(重点)
-
阐述明白一个锁升级的流程
-
锁的优缺点对比
-
-
-
原子操作的实现原理
原子(atomic)本意是“不能被进一步分割的最小粒子”,而原子操作意为“不可被中断的一个或一系列操作”。在多处理器上实现原子操作就变得有点复杂。
- 术语定义
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v6gXXcbP-1614419582166)(C:\Users\95793\Desktop\image-20210227114544051.png)]
-
处理器实现原子操作
- 保证复杂内存操作原子性
- 总线锁
使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占共享内存。 - 缓存锁
缓存锁定
- 总线锁
- 保证复杂内存操作原子性
-
java如何实现原子操作
锁和循环CAS的方式来实现原子操作。
- 使用循环CAS实现原子操作
- CAS实现原子操作的三大问题
- ABA问题
- 循环时间长开销大
- 只能保证一个共享变量的原子操作
- 使用锁机制实现原子操作
- 使用循环CAS实现原子操作
java内存模型的基础(不是面试重点)
并发编程模型的两个关键问题
线程之间如何通信及线程之间如何同步(这里的线程是指并发执行的活动实体)。通信是指线程之间以何种机制来交换信息。
- 线程之间如何通信
- 线程之间如何同步
通信机制
-
共享内存
在共享内存的并发模型里,线程之间共享程序的公共状态,通过写-读内存中的公共状态进行隐式通信。
-
消息传递
在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过发送消息来显式进行通信。
同步
同步是指程序中用于控制不同线程间操作发生相对顺序的机制。
- 在共享内存并发模型里,同步是显式进行的。程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行。
- 在消息传递的并发模型里,由于消息的发送必须在消息的接收之前,因此同步是隐式进行的。
java的并发采用的是共享内存
隐式通信、显示同步
堆内存在线程之间共享
局部变量,方法定义参数和异常处理器参数不会在线程之间共享,它们不会有内存可见性问题
**JMM:Java线程之间的通信由Java内存模型(本文简称为JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见 **
JMM定义了线程和主内存之间的抽象关系:
线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UEsuhfBa-1614419582167)(image-20210227141706295.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9RUotbq9-1614419582169)(image-20210227141833637.png)]
从源代码到指令序列的重排序
-
目的:提高性能
-
编译器和处理器进行重排序 分为3种类型
-
编译器优化的重排序
编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
-
指令集并行的重排序
处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
-
内存系统的重排序
由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上
去可能是在乱序执行[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HW52h1yi-1614419582170)(image-20210227143033336.png)]
1是编译器重排序,2、3是处理器重排序
-
-
对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。
-
对于处理器重排序,JMM的处理器重排序规则会要求Java编译器在生成指令序列时,插入特定类型的内存屏障指令,通过内存屏障指令来禁止特定类型的处理器重排序。
并发编程模型的分类
现代的处理器使用写缓冲区临时保存向内存写入的数据。写缓冲区可以保证指令流水线持续运行,它可以避免由于处理器停顿下来等待向内存写入数据而产生的延迟。同时,通过以批处理的方式刷新写缓冲区,以及合并写缓冲区中对同一内存地址的多次写,减少对内存总线的占用。虽然写缓冲区有这么多好处,但每个处理器上的写缓冲区,仅仅对它所在的处理器可见。这个特性会对内存操作的执行顺序产生重要的影响:处理器对内存的读/写操作的执行顺序,不一定与内存实际发生的读/写操作顺序一致
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BDrWcNbd-1614419582172)(image-20210227145310208.png)]
处理器A执行内存操作的顺序为:A1→A2,但内存操作实际发生的顺
序却是A2→A1。此时,处理器A的内存操作顺序被重排序了
由于写缓冲区仅对自己的处理器可见,它会导致处理器执行内存操作的
顺序可能会与内存实际的操作执行顺序不一致。
- 内存屏障类型表
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DQHnNXhY-1614419582173)(image-20210227150326351.png)]
happens-before简介
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KkfWTCEH-1614419582174)(image-20210227151345480.png)]
重排序
重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。
数据依赖性
如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HfcZm5Fn-1614419582175)(image-20210227151203512.png)]
如图,如果操作的执行顺序发生改变,那么执行结果就会改变。
仅针对单个处理器中执行的指令序列和单个线程中执行的操作,不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑。
as-if-serial语义
as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。
程序顺序规则
重排序对多线程的影响
顺序一致性
如果程序是正确同步的,程序的执行将具有顺序一致性——即程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同。
顺序一致性内存模型
volatile的内存语义
volatile的特性
一个volatile变量的单个读/写操作,与一个普通变量的读/写操作都是使用同一个锁来同步,它们之间的执行效果相同。
-
可见性
对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
-
原子性
对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。
volatile写-读建立的happens-before关系
从内存语义的角度来说,volatile的写-读与锁的释放-获取有相同的内存效果:volatile写和锁的释放有相同的内存语义;volatile读与锁的获取有相同的内存语义。
锁的内存语义
锁是Java并发编程中最重要的同步机制。
锁除了让临界区互斥执行外,还可以让释放锁的线程向获取同一个锁的线程发送消息。
线程A释放一个锁,实质上是线程A向接下来将要获取这个锁的某个线程发出了(线程A对共享变量所做修改的)消息。
线程B获取一个锁,实质上是线程B接收了之前某个线程发出的(在释放这个锁之前对共享变量所做修改的)消息。
线程A释放锁,随后线程B获取这个锁,这个过程实质上是线程A通过主内存向线程B发送消息。
final域的内存语义
单例模式(重点)
几种代码实现都要能实现