3 Java内存模型
3.1 Java内存模型基础
- 线程之间的通信:共享内存、消息传递
共享内存:通过读写内存中的公共状态进行隐式通信
消息传递:线程之间没有公共状态,通过发送消息来显式通信
- 线程之间的同步:程序中用于控制不同线程间操作发生的相对顺序的机制。
共享内存并发模型中,显式同步,程序员必须指定某个方法或某段代码需要在线程之间互斥进行。
消息传递并发模型中,隐式同步,因为消息的发送必须在消息接收之前。
- Java并发采用共享内存模型。
- Java中所有的实例域(对象中的数据)、静态域、数组元素都存储在堆内存中,堆内存在线程间共享;局部变量、方法定义参数、异常处理器参数不在线程间共享,因此不存在内存可见性问题。线程有自己的栈桢和寄存器。
- 内存模型JMM定义线程与主内存(抽象概念,涵盖了缓存、写缓冲区、寄存器等)之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程有一个私有的本地内存,用来存储该线程以读写共享变量的副本。JMM通过控制主内存与每个线程的本地内存之间的交互,来为java程序员提供内存可见性保证。
线程A、B的通信,要经过:A将本地内存中更新过的共享变量刷新到主内存——>B到主内存中读取A更新后的值。通信过程必须经过主内存。
- 执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。JMM确保在不同编译器和不同处理器平台上,通过禁止特定类型的编译器重排序和处理器重排序(内存屏障指令),为程序员提供一致的内存可见性保证。
- 编译器优化的重排序:编译器在不改变单线程程序语义的前提下重新安排语句执行顺序
- 指令级并行的重排序:如果不存在数据依赖,处理器可以改变语句对应机器指令的执行顺序
- 内存系统的重排序:处理器使用读/写缓冲区,使得处理器对内存的读写操作的执行顺序不一定与内存实际发生的读写顺序一致,因此现代处理器允许对读-写操作重排序。
- 现代处理器采用写缓冲区临时保存向内存写入的数据。
可以保证指令流水线持续进行,避免由于处理器停顿下来等待向内存写入数据而引起的延迟
可以采用批处理的方式刷新缓冲区,合并写缓冲区中对同一内存的多次写,减少对内存总线的占用。
每个处理器的写缓冲区仅对自己可见。
- 内存屏障:
- StoreLoad同时具有其他三个屏障的效果。
3.2 重排序
数据依赖性:
- 两个操作访问同一变量,其中有一个为写操作,则两个操作存在数据依赖性。包括:写后读、读后写、写后写。
- 如果对存在数据依赖性的操作重排序,则会改变执行结果,所以编译器和处理器不会改变存在数据依赖的操作的执行顺序。
这里的数据依赖性针对单个处理器中执行的指令序列,单个线程中执行的操作。
不同处理器之间、不同线程之间的数据依赖不被编译器和处理器考虑。
as-if-serial:
- 不管怎么重排序,程序的执行结果不能被改变。编译器、runtime和处理器必须遵守它的语义(不对存在数据依赖的操作进行重排序)。
happens-before:
- JDK5开始使用JSR-133内存模型,它使用happens-before来阐述操作之间的内存可见性。
- 如果一个操作执行结果需要对另一个操作可见,那么它们之间必须存在happens-before关系。
- A happens-before B并不要求A一定要在B之前执行,而是要求A的执行结果对B可见。
控制依赖性
- 当代码中存在控制依赖时,会影响指令序列执行的并行度,编译器和处理器采用猜测执行,
- 单线程程序中对存在控制依赖的操作重排序不会改变程序执行结果,但多线程程序中,可能会改变。
3.3 顺序一致性
- 顺序一致性模型:理想化的理论参考模型,在此模型中一个线程中所有操作必须按程序顺序执行;不管程序是否同步(同步锁),所有线程只能看到单一的操作执行顺序(整体的顺序所有线程可见一致),每个操作必须原子执行,且立即对所有线程可见。
JMM中不保证单线程的操作会按顺序执行,在正确同步时也会在临界区内重排序,未同步或未正确同步时只提供最小安全性(保证线程执行时读到的值要么是之前某线程写入的值,要么是默认值0,null,false,不会无中生有。即:JVM在堆上分配对象时首先会对内存空间清零,然后分配);
JMM中不保证long型和double型的变量写操作具有原子性。
- 总线事务:数据在处理器和内存之间传输,通过总线。
- 32位处理器中,64位变量(long/double)的读写可能会被拆分为两个32位的读写来执行,分配到不同的总线事务中,此时64位变量的读写不具有原子性。JSR-133之后读操作必须具有原子性。
3.4 volatile的内存语义
- volatile的写-读实现了线程间通信,volatile变量的读写具有原子性(++这种复合操作不具有原子性)
- JMM为实现volatile写-读,会分别限制编译器和处理器的重排序类型(编译器生成字节码时插入内存屏障)
- volatile重排序规则:volatile写之前的操作不会被编译器重排序到volatile写之后;volatile读之后的操作不会被编译器重排序到volatile读之前;第一个操作是volatile写,第二个操作是volatile读时,不能重排序。
JMM在每个volatile写后面插入一个StoredLoad屏障(通常是一个写线程多个读线程,为了执行效率,在写后面加屏障(也可以在读前面加屏障,效率低))
- JSR-133增强了volatile的内存语义,使得volatile的写-读拥有和锁的释放-获取一样的内存语义。
- volatile保证对单个volatile变量的读写具有原子性,而锁可以确保对整个临界区代码的执行具有原子性。
3.5 锁的内存语义
- 释放锁与volatile写语义相同(释放锁时JMM会把线程本地内存中的共享变量刷新到主内存中),加锁与volatile读语义相同。
- 内存实现:通过一个volatile变量来维护同步状态(公平锁、非公平锁)
- AQS:java同步器框架AbstractQueuedSynchronizer
- ReentrantLock中通过lock()获取锁,加锁轨迹如下:
- CAS同时具有volatile读和volatile写的内存语义。(分析源码,lock指令),以原子方式实现内存的读-改-写(现代处理器上支持的高效机器级别的原子指令)。
- concurrent包实现模式:声明共享变量为volatile,使用CAS的原子条件更新来实现线程之间的同步。
【待补充。。。】
3.6 final域
- 重排序规则(写、读)
- final域为引用类型时
3.7 happens-before
- 定义、规则(start规则、join规则);
- 实际上给程序员创造了一个幻境
3.8 双重检查锁定与延迟初始化
- 双重检查锁定用来延迟初始化是线程不安全的(实例没初始化完全)
- 基于volatile
- 基于类初始化