java多线程的内存模型
3.1java内存模型的基础:
介绍基本概念
- 并发编程的两个问题
通信和线程同步
通信是指线程之间以何种机制来交互信息:两种方式 共享内存 和 消息传递
1.共享内存的模型里,线程间通过共享程序的公共状态进行隐式通信
2.消息传递的模型里,线程通过发送消息进行通信
同步是指程序中用于控制不同线程操作发生顺序的机制
1.共享内存的模型里,同步间通过显示进行的,程序要显示指定某个方法或代码互斥执行
2.消息传递的模型里,消息发生必须要在消息接收前,同步是隐式进行的
java并发采用的是共享内存模型 - java内存模型的抽象结构
实例域,静态域,和数组元素(共享变量)都存储在堆内存中,线程共享
每个线程都有一个本地内存,本地内存中存储了该线程读写共享变量的副本,本地内存是JMM的抽象概念,并不真实存在,它涵盖了缓存,写缓冲区,寄存器以及其他硬件和编译器优化。
JMM通过控制主内存与每个线程的本地内存之间的交互,来为java程序员提供内存可见性保证 - 从源代码到指令序列的重排序
执行程序时,为了提高性能,编译器和处理器会对指令进行重排序,分为3中类型
1.编译器优化的重排序,编译器在不改变单线程语义的前提下,重排
2.指令级并行的重排序,处理器对机器指令的执行顺序
3.内存系统的重排序,处理器使用缓存和读写缓冲区,使得加载和存储操作看上去可能是乱序执行的
对于编译器的重排序规则1,JMM会禁止,对于处理器重排序,JMM会生成指令序列,插入内存屏障禁止重排序
4.并发编程模型的分类 - happens-before原则
在JMM中,如果一个操作的结果需要对另一个操作可见,那么两个操作必须要存在happens-before关系
程序顺序规则:一个线程的每一个操作先于另一线程的任意操作
监视器锁规则:对于一个锁的解锁,先于对这个锁的加锁
volatile变量规则:对一个volatile域的写先于对这个域的读
传递性:a先于b b先于c 则 a先于c
3.2 重排序
数据依赖性
as-if-serial语义
程序顺序规则
3.3 顺序一致性
是一个理想参考模型,在设计时,处理器的存储模型和编译语言的内存模型都会参照
- 数据竞争与顺序一致性
当程序未正确同步时,就可能会存在数据竞争,java中的定义:一个线程写,一个线程读,写和读没有通过同步来排序
这里的同步是广义的同步,包括常用的同步源语synchronized volatile final - 顺序一致性的内存模型
计算机科学家理想化的一个模型,给程序员提供了强内存可见性保证,两大特征
线程的所有操作必须按照程序的顺序执行
所有线程都只能看到一个单一的操作执行顺序,每个操作都必须是原子操作且对所有线程可见 - 同步程序的顺序一致性效果
使用个锁来实现,在JMM中,临界区内的代码可重排序,JMM会在退出和进入临界区时做处理,使得线程具有顺序一致性
3.4 volatile的内存语义
- volatile特性 禁止指令重排,内存可见性 原子性(对于单个读写操作)
- 写-读建立的happens-before关系
volatile对线程的内存可见性的影响需要关注,volatile变量可以实现线程间通信,volatile的写和锁的释放有相同的语义,读和锁的获取有相同的语义
- volatile的读写的内存语义
当写一个volatile变量时,JMM会把本地变量刷新到主内存,当读一个volatile变量时,JMM会把线程对应的本地内存置为无效,从主内存中重新读取。
1).线程A在写一个volatile变量,实质是线程向接下来要读这个变量的线程发送一个消息
2).线程B读volatile变量,实质是接收到之前的某个线程发出的消息(在写volatile变量之前对共享变量的修改)
3).线程A写volatile变量,线程B读volatile变量,这个过程实质是线程A通过主内存想线程B发送消息 - 内存语义的实现
为了实现volatile的内存语义,编译器使用内存屏障来禁止处理器重排序,JMM内存屏障插入策略
在volatile的写操作前面加StoreStore屏障
在volatile的写操作后面加StoreLoad屏障
在volatile的读操作前面加LoadLoad屏障
在volatile的读操作后面加LoadStore屏障
3.5 锁的内存语义
- 锁的释放获取建立的happens-before关系
锁是重要的同步机制, 锁可以让临界区互斥执行外,还可以让释放锁的线程向获取锁的线程发送消息
在图中,线程A释放锁后,随后线程B获取同一个锁,在图总 2happens-before5,因此线程A释放锁之前所有可见的共享变量,在线程B获取同一个锁之后将立刻变得对线程B可见 - 锁释放和获取的内存语义
释放锁时,会把共享变量刷新到主内存中,当线程获取锁时,JMM会把线程对应的本地内存置为无效,从而使得被监视器保护的临界区代码必须从主内存中读取共享变量
1).线程A释放锁,实质是线程A向接下来将要获取锁的某个线程发出消息(线程A对变量的修改)
2).线程B获取锁,实质是线程B接收之前A线程发出的消息
3).线程A释放锁,线程B获取锁,这个过程实质是线程A通过主内存向线B发送消息
- 内存语义的实现
使用ReentrantLock探索过程,ReentrantLock锁的实现依赖AQS:使用一个整型volatile来维护同步状态,加锁时首先读取volatile变量state,解锁方法写state,使用cas方式设置state
锁的执行字节码天机lock前缀:
1)确保读改写操作的原子性,Intel使用缓存锁定来保证指令的原子性,大大降低执行开销
2)禁止该指令,与之前好之后的读和写指令重排序
3)把写缓冲区中的所有数据刷新到内存中
锁释放和获取的内存语义实现方式:利用volatile变量的读写 利用cas操作带有的volatile读写的内存语义 - Concurrent包的实现
CAS操作会使用处理器提供的高效机器级别的原子指令,在concurrent·包中,一个通用化的实现模式 声明共享变量volatile,然后使用CAS操作实现线程同步,同时配合读写带来的内存读写语义实现线程通信
3.6 final域的内存语义
- final域的重排序规则
1)在构造函数内对一个final域的写入,域随后把这个被构造函数对象的引用赋值给一个引用变量,两个操作不能重排序
2)初次读一个final域的对象的引用,与随后读取这个final域,这两个操作之间不能重排序 - 写
JMM禁止把final域的写重排序到构造函数之外,编译器会在构造函数的return之前,插入一个StoreStore屏障 - 读
在一个线程中初次读取对象引用和初次读取这个对象包含的final时,JMM禁止处理器重排序这两个操作,会在读final域操作的前面加个LoadLoad屏障 - final域是引用类型是不能保证重排序
3.7 happens-before
JMM的核心概念,理解happens-before是理解JMM的关键
happens-before规则和as-if-serial规则语义相似,as-if-serial语义保证单线程程序的执行结果不被改变,happens-before保证正确同步的多线程程序的执行结果不被改变
JMM的设计:
程序员对内存模型的使用,易于理解易于编程,基于一个强内存模型编写代码;
编译器和处理器对内存模型的实现。编译器和处理器希望内存模型对它们的约束越来越少
重排序分为会改变结果的重排序和不会改变结果的重排序;对于会改变结果的要求禁止
happens-before规则:
3.8 双重检查锁定与延迟初始化
3.9 内存模型综合描述
顺序一致性内存模型是理论参考模型,JMM和处理器内存模型在设计时会参考。
JMM的内存可见性保证:
1.单线程程序,不会出现内存可见性问题,编译器和处理器会共同保证单线程程序的执行结果
2.正确同步的多线程程序,将具有顺序一致性。这里是JMM关注的重点,JMM通过限制编译器和处理器的重排序为程序员提供保证内存可见性
3.未同步的多线程程序,JMM为它们提供最小的安全保障:线程在读取的值,要么是之前某个线程写入的值,要么是默认值
[1]: Java并发编程的艺术