内存模型
Java 内存模型
1、Java Memory Model(JMM)
2、JMM 在多线程读写共享数据(成员变量、数组)时,定义一套对数据的可见性、有序性、原子性的规则和保障
(1)原子性:保证指令不会受到线程上下文切换的影响
(2)可见性:保证指令不会受 CPU 缓存的影响
(3)有序性:保证指令不会受 CPU 指令并行优化的影响
3、定义主存(共享内存)、工作内存(线程私有)抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、 CPU 指令优化等
原子性
1、synchronized
synchronized (锁对象) {
原子操作代码
}
2、每个对象都有一个 monitor 区
(1)只有在添加 synchronized 关键字,调用对象 / 方法时才生效
(2)Owner:monitor 的所有者,同一时刻,只有一个线程成为 Owner
(3)EntryList:阻塞区,当有线程占用 Owner 时,其他线程在该区阻塞
(4)WaitSet:当前线程执行 wait 方法,就会释放锁,进入阻塞状态,该线程存放在 WaitSet
(5)线程进入 Owner,执行 JVM 指令 monitorenter;线程退出 Owner,执行 JVM 指令 monitorexit
(6)释放锁时,EntryList 的多个线程进行增强
3、重入锁:Java 支持对同一对象多次 monitorenter、monitorexit
可见性
1、volatile
(1)修饰成员变量、静态成员变量
(2)作用:避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存
(3)保证在多个线程之间,一个线程对 volatile 变量的修改,对另一个线程可见
(4)不能保证原子性,仅用在一个写线程,多个读线程的情况
2、synchronized
(1)既可以保证代码块的原子性,也同时保证代码块内变量的可见性
(2)缺点:属于重量级操作,性能相对更低
有序性
1、指令重排
(1)在程序结果不受影响的前提下,可以调整指令语句执行顺序
(2)多线程下指令重排会影响正确性
2、volatile 修饰的变量,可以禁用指令重排,禁止的是加 volatile 关键字变量之前的代码重排序
volatile
1、底层实现原理是内存屏障,Memory Barrier(Memory Fence)
(1)对 volatile 变量的写指令后会加入写屏障
(2)对 volatile 变量的读指令前会加入读屏障
2、保证可见性
(1)写屏障(sfence)保证在该屏障之前,对共享变量的改动,都同步到主存当中
(2)读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据
3、保证有序性
(1)写屏障确保指令重排序时,不会将写屏障之前的代码,排在写屏障之后
(2)读屏障确保指令重排序时,不会将读屏障之后的代码,排在读屏障之前
(3)注意:volatile 不能解决指令交错
(4)写屏障只保证之后的读,能够读到最新的结果,但不能保证其它线程的读在本线程前
(5)有序性的保证只保证本线程内相关代码不被重排序
4、更底层是读写变量时,使用 lock 指令,保证多核 CPU 之间的可见性、有序性
5、synchronized 不能禁止代码块内的指令重排
(1)但可以保证其有序性
(2)原因:synchronized 保证单线程执行代码块,不会出现多线程下的指令重排问题
(3)即有序性建立在原子性基础上
happens-before
1、规定哪些写操作对其它线程的读操作可见,是可见性与有序性的一套规则总结
2、抛开以下 happens-before 规则,JMM 并不能保证一个线程对共享变量的写,对于其它线程对该共享变量的读可见
3、以下变量都是指成员变量 / 静态成员变量
(1)t1 线程释放锁对象之前,对变量的写,对于接下来对 m 加锁的 t2 线程,对该变量的读可见
(2)t1 线程对 volatile 变量的写,对接下来 t2 线程,对该变量的读可见
(3)t1 线程 start 前,对变量的写,对 t1 线程开始后,对该变量的读可见
(4)t1 线程结束前,对变量的写,对 t2 线程得知它结束后的读可见(如:其它线程调用其 isAlive() 或 join(),等待它结束)
(5)线程 t1 打断(interrupt)t2 线程前,对变量的写,对于 t3 线程得知 t2 被打断后,对变量的读可见(通过 t2.interrupted 或 t2.isInterrupted)
(6)对变量默认值(0,false,null)的写,对其它线程对该变量的读可见
(7)具有传递性,如果 x happens-before -> y 并且 y happens-before -> z,那么有 x happens-before -> z
CAS
1、Compare and Swap
2、一种乐观锁的思想
(1)CAS 基于乐观锁的思想
(2)synchronized 基于悲观锁的思想
3、获取共享变量时,为了保证该变量的可见性,需要使用 volatile 修饰
(1)结合 CAS 和 volatile 可以实现无锁并发,适用于竞争不激烈、多核 CPU 的场景下
(2)因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一
(3)但如果竞争激烈,重试必然频繁发生,反而影响效率
4、CAS 底层:依赖于一个 Unsafe 类来直接调用操作系统底层的 CAS 指令
5、原子操作类
(1)juc(java.util.concurrent)中提供了原子操作类,可以提供线程安全的操作,例如:AtomicInteger、AtomicBoolean 等
(2)底层就是采用 CAS 技术 + volatile 实现
synchronized 优化
1、Java HotSpot 虚拟机中,每个对象都有对象头(包括 class 指针、Mark Word)
(1)Mark Word 平时存储这个对象的哈希码、分代年龄
(2)当加锁时,(1)信息就根据情况被替换为标记位、线程锁记录指针、重量级锁指针、线程ID 等
2、轻量级锁
(1)如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(即没有竞争),那么可以使用轻量级锁来优化
(2)如果这期间有其它线程 B,告知线程 A 有并发访问,线程 A 随即升级为重量级锁,进入重量级锁的流程
(3)每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的 Mark Word
3、锁膨胀
(1)如果在尝试加轻量级锁的过程中,CAS 操作无法成功
(2)一种情况是有其它线程为此对象加上轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁
4、重量锁
(1)重量级锁竞争时,还可以使用自旋来进行优化,如果当前线程自旋成功,即这时持锁线程已经退出了同步块,释放了锁,这时当前线程就可以避免阻塞
(2)在 Java 6 之后自旋锁是自适应的,比如:对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,则少自旋甚至不自旋
(3)自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势
(4)Java 7 之后不能控制是否开启自旋功能
5、偏向锁
(1)轻量级锁在没有竞争时,自身线程每次重入,仍然需要执行 CAS 操作
(2)Java 6 中引入了偏向锁:只有第一次使用 CAS,将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS
(3)撤销偏向:需要将持锁线程升级为轻量级锁,这个过程中所有线程需要暂停(STW)
(4)访问对象的 hashCode 也会撤销偏向锁
(5)如果对象虽然被多个线程访问,但没有竞争,这时偏向线程 T1 的对象,仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID
(6)撤销偏向、重偏向都是批量进行的,以类为单位
(7)如果撤销偏向到达某个阈值,整个类的所有对象都会变为不可偏向
(8)可以主动使用 -XX:-UseBiasedLocking 禁用偏向锁
6、减少上锁时间:同步代码块中尽量短
7、减少锁的粒度:将一个锁拆分为多个锁提高并发度
(1)ConcurrentHashMap
(2)LongAdder 分为 base 和 cells 两部分。没有并发争用时,或 cells 数组正在初始化时,会使用 CAS 来累加值到 base,有并发争用,会初始化 cells 数组,数组有多少个 cell,就允许有多少线程并行修改,最后将数组中每个 cell 累加,再加上 base 就是最终的值
(3)LinkedBlockingQueue 入队、出队使用不同的锁,相对于 LinkedBlockingArray 只有一个锁效率要高
8、锁粗化
(1)多次循环进入同步块,不如同步块内多次循环
(2)另外 JVM 可能会做如下优化,把多次 append 的加锁操作粗化为一次,因为都是对同一个对象加锁,没必要重入多次
new StringBuffer().append("a").append("b").append("c");
9、锁消除
(1)JVM 会进行代码的逃逸分析
(2)例如:某个加锁对象是方法内局部变量,不会被其它线程所访问到,这时就会被即时编译器忽略掉所有同步操作
10、读写分离
(1)CopyOnWriteArrayList
(2)ConyOnWriteSet
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战