前言
在并发编程中,一般需要处理两个关键问题:线程通信、线程同步,然而线程通信一般的方式有:共享内存、消息传递。
对于共享内存的通信方式,线程同步是显示进行的,程序员必须指定某个方法或者某个代码段需要在线程间互斥进行。对于消息传递的通信方式,线程同步是隐式进行的,因为消息的接收一定在发送之后。Java采用的是共享内存的通信方式。
基于java是采用共享内存的线程通信方式 ,因此我们有必要研究Java的内存模型,防止被莫名其妙的内存可见性问题困扰。
JMM模型
从抽象的角度来看,线程之间共享的变量存储在主内存中,每个线程都有一个私有的本地内存,存储着该线程读取/修改的变量的副本。本地内存是JMM的一个抽象的概念
从上图可以看出,线程A和线程B通信就要求,线程A先把本地内存更新过的变量更新到主内存中,然后线程B去主内存读取线程A更新了的变量。整体上来看,JMM就是通过控制主内存和每个线程的本地内存之间的交互,来为Java程序员提供内存可见性保证。
指令序列重排序
在执行程序,为了提高性能,编译器和处理器常常会对指令做重排序,重排序分为以下三类:
- 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句
的 执 行 顺 序
- 指令级并行的重排序。将多条指令重叠执行,如果不存在数据依赖性,处理器可以改变语句对应的机器指令的执行顺序
- 内存系统的重排序。由于处理器使用了缓存区,这使得加载操作和存储操作看上去可能是杂乱进行的。
上述1属于编译器重排序,2和3属于处理器重排序,JMM在处理重排序的规则的时候,会要求Java编译器在生成指令序列时,插入特定类型的内存屏障指令,通过内存屏障指令来禁止特定类型的处理器重排序。
现在的处理器都会用到高速缓冲区存储数据,这样可以有效避免由于处理器停顿下来等待向内存写入的数据而产生的延迟,同时也可以批量刷新缓存提高效率,减少对内存线的占用。在多核处理器盛行的时代,每个处理器都有相应的高速缓冲区,这样同样也为并发编程带来了挑战,与上面JMM模型一样,处理器也存在缓存和内存数据不一致的情况。
这里的关键是,由于写缓冲区仅对自己的处理器可见,它会导致处理器执行内存操作的 顺序可能会与内存实际的操作执行顺序不一致。由于现代的处理器都会使用写缓冲区,因此现代的处理器都会允许对写-读操作进行重排序。
常见处理器允许的重排序类型如下:
其中N代表不允许两个操作重排序,Y代表允许两个操作重排序。
Load:装载数据(从内存中读取数据)
Store:存储数据(存储数据到内存)
内存屏障
为了保证内存可见性,Java编译器在生成指令的适当位置会插入内存屏障指令来禁止特定类型的重排序。
内存屏障类型:
StoreLoad Barriers 是一个 “ 全能型 ” 的屏障,它同 时 具有其他 3 个屏障的效果。 现 代的多 处理器大多支持 该 屏障(其他 类 型的屏障不一定被所有 处 理器支持)。 执 行 该 屏障开 销 会很昂贵 ,因 为 当前 处 理器通常要把写 缓 冲区中的数据全部刷新到内存中( Buffer Fully Flush )。