【学习笔记】Java并发编程的艺术——第3章 Java内存模型

发布于:2025-08-14 ⋅ 阅读:(21) ⋅ 点赞:(0)

第3章 Java内存模型

3.1 Java内存模型的两个关键问题

1>线程通信
①共享内存
线程共享程序的公共状态,读-写内存中的公共状态隐式通信
②消息传递
线程之间没有公共状态,需通过发送消息进行显式通信
2>线程同步
①共享内存下同步显式进行,由程序员指定互斥代码
②消息传递先发送后接收,同步隐式进行
【Java为共享内存模型】

3.1.2 Java内存模型的抽象结构

在这里插入图片描述
线程AB通信时:
①A修改本地内存中共享变量的值
②JMM控制下,将A本地内存中的共享变量写回主内存
③B将主内存的共享变量值读到本地内存(JMM控制),获取值
【JMM控制主存与本地内存之间的交互】

3.1.3 从源代码到指令序列的重排序

为了提高性能,编译器与处理器会对指令做3种类型的重排
1>编译器优化的重排序
不改变单线程程序语义下,可重新安排指令的执行顺序
2>指令级并行的重排序
不存在数据依赖性,处理器可以将多条指令重叠进行
3>内存系统的重排序
处理使用缓存与读/写缓冲区,使加载与存储操作看上去可能在乱序执行
【JMM会禁止特定类型的编译器和处理器指令重排】

3.1.4 并发编程模型的分类

处理器具有写缓冲区,导致写操作实际上是先写入缓冲区,再读时,读操作结束了,但仍未将写缓冲区内容写入主内存,故会存在指令重排问题,JMM通过内存屏障禁止读操作

3.1.5 happens-before简介

该规则保证了前一个操作对后一个操作是可见的
1>程序顺序规则
一个线程中的每个操作h-b于之后任意操作
2>监视器锁规则
解锁h-b于加锁
3>volatile
对于volatile写h-b于读
4>传递性

3.2 重排序

编译器和处理器为了优化程序性能对指令序列重新排序的手段

3.2.1 数据依赖性

在两个操作中,涉及针对同一个变量的写操作,改变指令顺序便会影响最终执行结果,这就是数据依赖性
编译器与处理器不会改变,单个处理器/单个线程中的数据依赖性
【在计算机中,软件技术和硬件技术有一个共同的目标,在不改变程序执行结果前提下尽可能提高并行度】

3.2.2 as-if-serial语义

不管如何排序,单线程的执行结果不能改变

3.2.3 程序顺序规则

在①中,A h-b于 B,C B h-b于 C
A与B可重排是因为A不需要对B可见,B并不关心A是否改变

3.2.4 重排序对多线程的影响

①因为B线程对A线程的数据是有顺序依赖的,所以重排序会影响多线程语义
②控制依赖:处理器将if中的语句执行并缓存,如果为true,则从缓存中读取结果
③控制依赖不破坏单线程执行结果,但会破坏多线程执行结果

3.3 顺序一致性

3.3.1 数据竞争与顺序一致性

一个线程写,另一个线程读,没有通过同步来排序

3.3.2 顺序一致性内存模型

1>一个线程中的所有操作必须按照程序的顺序来执行
2>每个操作都必须原子执行且立刻对所有线程可见

同一时间只有一个线程操作内存,任务执行顺序对于每个线程都是相同的,可见的。JMM只有在正确使用同步原语时,程序才保证顺序一致性。

3.3.3 同步程序的顺序一致性

P35

3.3.4 未同步程序的执行特性

JKD5之后,一个long/double类型的写操作不具有原子性

3.4 volatile的内存语义

3.4.1 volatile的特性

1>可见性:任意读操作总能看到上一个最后的写操作值
2>原子性:任意对volatile的读/写具有原子性,但volatile的符合操作,不具有原子性。

3.4.2 volatile 写-读建立的happens-before关系

3.4.3 volatile写-读的内存语义

1>volatile写时,会在修改完变量后将线程全部的共享变量刷入主存
2>volatile读时,会将缓存中的全部共享变量失效,然后去主存取
3>所以volatile的写总是对读可见

3.4.4 volatile内存语义实现

1>volatile写之前的操作不能重排到volatile之后,因为要一起刷回主内存
2>volatile读之后的操作不能重排到volatile之前,因为要一起从主内存重新加载
3>volatile读写不能重排

3.4.5 JSR为什么增强volatile语义

1>轻量级通信需求
在 Java内存模型 (JMM)中,volatile变量通过主存直接交互实现线程间通信,而锁需要通过 同步块 (synchronized)实现。锁的互斥执行特性虽然能保证临界区代码的原子性,但开销较大,不适合所有场景。因此,JSR-133通过增强volatile语义,使其写-读操作与锁的释放-获取具有相同内存语义,从而提供更轻量级的线程通信方式。 ‌

2>可见性与有序性保障
旧模型中,volatile变量与普通变量之间允许重排序,可能导致变量值传递延迟或执行顺序混乱。JSR-133严格限制编译器和处理器对volatile变量与普通变量的重排序,确保volatile写-读操作与锁的释放-获取具有相同内存语义。此外,通过内存屏障机制禁止指令重排序,保障了volatile变量操作的原子性和有序性。

3.5 锁的内存语义

锁的释放对获取可见

3.5.2 锁释放和获取的内存语义

1>释放锁时将共享变量写入内存
2>获取锁时将线程的本地内存设置为无效

3.5.3 锁内存语义的实现

1>公平与非公平锁释放时通过写volatile刷新内存
2>非公平加锁时通过CAS volatile同时实现刷新内存与让本地内存失效
3>公平加锁时通过读volatile使本地内存失效

3.5.4 concurrent包的视线

1>声明共享变量为volatile
2>使用CAS原子条件更新实现线程之间的同步
3>配合volatile读/写与CAS volatile内存语义完成线程之间的通信

3.6 final域的内存语义

3.6.1 final域的重排序规则

1>final域的写入与引用赋值不可重排序
2>初次读一个包含final域对象的引用,与随后初次读这个final域,这两个操作之间不能重排序

3.6.2 写final域的重排序规则

写final域时,写入操作不可重排序到函数构造之外,否则B线程读取到A线程时,A线程中的final肯呢个还未赋值

3.6.3 读final域的重排序规则

读到对象引用final必定有值

3.6.4 final域为引用类型

另一个线程只要读到其他线程的对象引用,则该对象构造函数中对于final域的操作必定已完成

3.6.5 略

3.6.6 略

3.6.7 JSR-133 为什么增强final的语义

防止另一个线程第一次获取到final类型的初始化前的默认值覆盖另一个线程第二次获取到final初始化之后的值

3.7 happens-before

让程序员感觉到程序是按照h-b顺序执行的

3.7.3 happens-before

1>程序顺序:一个线程中的每个操作,前h-b于后
2>监视器规则:对于锁,释放h-b加锁
3>volatile:读h-b写
4>传递性
5>start():A线程中start了B线程,那么start操作h-b于B线程中所有操作
6>join():A线程中joinB线程,那B中全部操作h-b于join操作

3.8 双重检查锁定与延迟初始化

懒汉单例演变
①synchronized锁一切
②双重检查锁定
③锁get单利方法
④对象为null时,才锁进行创建,这样创建好后便不需要加锁了

3.8.2 问题根源

线程B在判断,对象不为null时,可能获取得到尚未初始化的对象,这是因为指令重排的存在

3.8.3 基于volatile的解决方案

禁止指令重排:
①为Instence加上volatile
②我认为可以给Instence变量加final

3.8.4 基于类初始化的解决方案

使用类初始化静态变量方法P72
相当于给初始化时加锁,并在初始化完成后释放

3.9 Java内存模型综述

越容易编程的语言内存模型越强,禁止的编译器、处理器优化越多,执行性能影响越大
【类初始化时机:
1>实力被创建
2>声明的静态方法被调用
3>声明的静态字段被赋值
4>声明的静态变量字段被使用(常量引用时也会初始化)
5>是一个顶级类】


网站公告

今日签到

点亮在社区的每一天
去签到