在并发编程中两个重要的问题,线程间如何通信及线程间如何进行同步。通信指的是线程间基于何种机制来交换信息,在命令是编程中,线程通信的机制有两种,共享内存和消息传递.
在共享内存的并发模型中,线程间共享程序的公共状态,通过读-写内存的公共状态进行隐式通信。在消息传递的模型中,线程间通过发送消息来显示通信。
同步指的是控制不同线程间操作发生的相对顺序。在共享内存模型中,同步是显示进行的,程序员必须显示的指定某段代码在线程中互斥执行,在消息传递并发模型中,消息的传递优先于消息的接受,因而同步是隐式进行的。
java内存模型的抽象结构
在java中,所有实例域、静态域和数组元素都存放在堆内存中,堆内存在线程中共享,局部变量和异常处理器参数不会在线程之间共享,不存在内存可见性问题。不会受内存模型的影响。
java线程之间的通信由java内存模型控制(简称为jmm),jmm决定了一个线程对共享变量的写入对另一个线程何时可见。jmm定义了线程和主内存间的抽象关系: 线程之间的共享变量存放在主内存中,每一个线程一个私有的本地内存,本地内存中存放的是读写共享变量的副本。如下图所示:
线程A和线程B要通信的话,经历两个步骤:
1> 线程A将更新的值写入到本地内存中,刷新到主内存中
2> 线程B从主内存中读值到本地内存中
也就是,jmm通过控制主内存和每个线程本地内存的交互,来实现共享内存。
从源代码到指令序列的重排序
在执行程序时,为了提高性能,编译器和处理器常会对指令做重排序,重排序分为3中类型
1> 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排重排序
2> 指令级并行的重排序。现代处理器采用了指令级并行技术将多条指令重叠执行。如果不存在数据依赖性,可以改变对应机器指令的执行顺序。
3> 内存系统的重排序。 由于处理器使用缓存和读写缓存区,使得加载和存储操作看起来像是乱序执行
从源代码到最终执行的指令序列示意图
1属于编译器重排序,2和3属于处理器重排序,这些重排序会导致出现内存可见性问题
jmm会禁止某些编译器的重排序,对于处理器重排序,jmm会要求java编译器在生成指令序列时候,插入指定的内存屏障,来禁止特定类型的处理器重排序。
总之,jmm通过禁止特性类型的编译器重排序和处理器重排序,来保证内存可见性。
并发编程模型的分类
现代处理器使用写缓存区临时保存向内存中写入的数据。写缓存区能够保证指令流水线持续运行,可以避免由于处理器提顿下等待向内存中写入数据产生的延迟。同时,通过以批处理的方式刷新写缓存区,以及合并缓存区中对同一内存地址的多次修改,减少对内存总线的占用。
结果,就是处理器对内存读/写操作的执行顺序,不一定与内存中实际发生的读/写顺序一致。
处理器操作内存出现结果示例
| processA | processB | |
| 代码 | a=1; x=a; |
b=2; y=b; |
| 运行结果 | a=b=0;是初始值 执行的结果可能就是 x=y=0; |
出现这种问题的原因:
从内存操作实际发生的情况看,当处理器A执行A3来刷新自己的写缓存区,写操作A1才算完成,处理器执行内存的顺序是A1->A2,但是实际内存的执行顺序A2->A1,此时内存系统发生了重排序,导致这种结果,处理器B同理。
为了保证内存可见性,jmm要求java编译器在指令序列适当的位置插入内存屏障禁止特定类型的处理器重排序。
| 屏障类型 | 指令示例 | 说明 |
| LoadLoad Barriers | Load1;LoadLoad;Load2 | 确保Load1的装载先于Load2及所有后续装载指令的装载 |
| StoreStore Barriers | Store1;StoreStore;Store2 | 确保Store1数据对其他处理器可见(刷新到内存)先于store2及后续存储指令的存储 |
| LoadStore Barriers | Load1;LoadStore;Store2 | 确保Load1的装载先于store2及后续指令刷新到内存 |
| StoreLoad Barriers | Store1;StoreLoad;Load2 | 确保Store1数据对其他处理器变的可见先于load2及后续指令的装载。 这个屏障会使得之前所有的内存访问指令完成才去执行屏障后的指令 具有其他3个屏障的效果 |
happens-before 简介
从jdk5开始,java使用新的jsr-133内存模型。JSR-133使用happens-before原则来阐述操作间的内存可见性。
在jmm中,如果一个操作的执行结果对另一个操作的执行结果可见,那么这两个操作间必须存在happens-before原则,这两个操作可以在一个线程中,也可以在不同线程间。
happens-before 原则:
1> 程序顺序规则:一个线程中,按照代码顺序,书写在前面的操作先行发生于线程中任意后续操作(时间上的先后)
2> 监视器锁规则:一个锁的解锁,先行发生对这个锁的加锁(时间上的先后)
3> Volatile变量规则:对一个volatile域的写,先行发生任意后续对这个域的读
4> 传递性: a happen before b b happen before c a ..c
5> 线程启动规则:线程A执行ThreadB.start() 启动线程B,那么A线程的ThreadB.start()操作happen-before线程b中的任意操作
6> 线程终止规则:线程A执行ThreadB.join()操作成功返回,那么线程B中的操作happen-before线程A从ThreadB.join()操作成功返回
note:
两个操作间具有happens-before关系,并不意味着前一个操作在后一个操作前执行,happens-before仅仅要求前一个操作的执行结果对后一个操作的执行结果可见,且前一个操作按照顺序上排在第二个操作之前。
java内存模型jmm对内存可见性的保证
java程序中内存可见性保证分为3类:
1> 单线程程序。单线程程序不会出现内存可见性问题,编译器和处理器能够保证单线程程序的执行结果和顺序一致性模型中的执行结果一致
2> 正确同步的多线程程序。正确同步的多线程程序将具有顺序一致性,jmm通过限制编译器和处理器的重排序来保证内存可见性。
3> 未同步/没有正确的同步的多线程程序。jmm为它们提供了最小安全保障:线程执行时候读取到的值,要么是之前某个线程写入的值,要么是默认值。