内存模型

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

posted @   半条咸鱼  阅读(44)  评论(0编辑  收藏  举报
(评论功能已被禁用)
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
点击右上角即可分享
微信分享提示