Java内存模型
线程间如何通信以及如何同步?
线程通信
通信是指线程之间以何种机制来交换信息。在命令式编程中,线程之间的通信机制有两种:共享内存和消息传递。
-
共享内存
在共享内存的并发模型里,线程之间共享程序的公共状态,通过写-读内存中的公共状态进行隐式通信。
- 消息传递
线程之间没有公共状态,线程之间必须通过发送消息来显式进行通信。
线程同步
同步是指程序中用于控制不同线程间操作发生相对顺序的机制。
有点拗口,我的理解就是多线程之间存在并发问题,线程之间很难保持顺序,就像车道上的汽车,有些快有些慢,同步就是由多车道变为单车道,使线程并行执行改为串行执行,串行后能保证线程的顺序。
在共享内存并发模型里,同步是显式进行的。程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行。
就是使用synchronized或者ReentrantLock。
在消息传递的并发模型里,由于消息的发送必须在消息的接收之前,因此同步是隐式进行的。
Java的并发采用的是共享内存模型,Java线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。
内存模型
java的堆内存是线程共享的。堆内存中存放着类的实例域、静态域和数组元素。
在方法中执行User user = new User()
图不一定正确,只是按目前的理解画的,要看完《深入了解Java虚拟机》才能对内存模型有更深入的理解。
局部变量(Local Variables),方法定义参数(Java语言规范称之为Formal Method Parameters)和异常处理器参数(Exception Handler Parameters)不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的影响。
Java线程之间的通信由Java内存模型本文简称为JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。
线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。
抽象结构示意图(出自《Java并发编程艺术》)
线程A与线程B通信:
1)线程A把本地内存A中更新过的共享变量刷新到主内存中去。
2)线程B到主内存中去读取线程A之前已更新过的共享变量。
线程A与线程B都会在各自的本地内存中缓存一份主内存中的共享变量副本,线程A修改共享变量的值,线程B会感知到主内存中的共享变量发生变化,从而将修改后的值重新读取到线程B的本地内存中达到线程通信的目的。两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存。JMM通过控制主内存与每个线程的本地内存之间的交互,来为Java程序员提供内存可见性保证。
重排序
重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段,重排序分3种类型。
1)编译器优化重排序:
在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
2)指令级并行的重排序:
现代处理器采用了指令级并行技术(Instruction-LevelParallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
3)内存系统的重排序:
由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
Java编译器在生成指令序列时,插入特定类型的内存屏障(Memory Barriers,Intel称之为Memory Fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序。
从源码到最终执行的指令序列的示意图(Java并发编程艺术)
上述的1属于编译器重排序,2和3属于处理器重排序。重排序可能会导致多线程程序出现内存可见性问题。对于处理器重排序,JMM的处理器重排序规则会要求Java编译器在生成指令序列时,插入特定类型的内存屏障指令,通过内存屏障指令来禁止特定类型的处理器重排序。
JMM属于语言级的内存模型,由于不同平台会有不同指令集内存操作,所以JMM通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。即JMM对程序员来说是平台无关的。
并发编程模型分类
现代的处理器使用写缓冲区临时保存要写入内存的数据。写缓冲区可以保证指令流水线持续运行,避免由于处理器停顿下来等待向内存写入数据而产生的延迟。同时,通过以批处理的方式刷新写缓冲区,以及合并写缓冲区中对同一内存地址的多次写,减少对内存总线的占用。
但每个处理器上的写缓冲区,仅仅对它所在的处理器可见。这个特性会对内存操作的执行顺序产生重要的影响:处理器对内存的读/写操作的执行顺序,不一定与内存实际发生的读/写操作顺序一致。
处理器操作内存的执行结果
处理器和内存的交互
由于Java使用共享内存进行线程通信,因为指令重排,A2可能会在A1之前执行,B2在B1之前执行,我们知道,如果int不指定初始值,则默认值为0,所以可能出现x=y=0的情况
假设处理器A和处理器B按程序的顺序并行执行内存访问,
数据依赖性
两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。而重排序是不会改变存在数据依赖关系的两个操作的执行顺序的。
数据依赖性
happens-before
在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。
happens-before只是描述内存可见性的,不是指前一个操作必须在后一个操作之前执行。
- 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
- 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
- volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
- 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
参考:
《Java编发编程艺术》