背景:在并发编程中,需要处理两个关键问题:线程之间如何通信及线程之间如何同步(这里的线程是指并发执行的活动实体)。通信是指线程之间以何种机制来交换信息。在命令式编程中,线程之间的通信机制有两种:共享内存和消息传递。在共享内存的并发模型里,线程之间共享程序的公共状态,通过写- 读内存中的公共状态进行隐式通信。在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过发送消息来显式进行通信。Java的并发采用的是共享内存模型,Java线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。如果编写多线程程序的Java程序员不理解隐式进行的线程之间通信的工作机制,很可能会遇到各种奇怪的内存可见性问题。
java内存模型:Java虚拟机规范中试图定义一种Java内存模型来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。Java内存模型规定了所有的变量都存储在主内存(Main Memory)中。每条线程还有自己的工作内存(Working Memory),线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成,线程、主内存、工作内存三者的交互关系如图所示。
结合例子来看,如果线程A与线程B之间要通信的话,必须要经历下面2个步骤。 1)线程A把本地内存A中更新过的共享变量刷新到主内存中去。 2)线程B到主内存中去读取线程A之前已更新过的共享变量。
从整体来看,这两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存。JMM通过控制主内存与每个线程的本地内存之间的交互,来为Java程序员提供内存可见性保证。
内存间交互操作:关于主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存之类的实现细节,Java内存模型中定义了以下8种操作来完成,虚拟机实现时必须保证下面提及的每一种操作都是原子的、不可再分的(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许有例外)。
这里需要补充一下java并发常被提起3个特性,原子性,可见性,有序性。
原子性:提供了互斥访问,同一时刻只能有一个线程来对他进行操作,由Java内存模型来直接保证的原子性变量操作包括read、load、assign、use、store和write,我们大致可以认为基本数据类型的访问读写是具备原子性的,如果应用场景需要一个更大范围的原子性保证(经常会遇到),Java内存模型还提供了lock和unlock操作来满足这种需求,尽管虚拟机未把lock和unlock操作直接开放给用户使用,但是却提供了更高层次的字节码指令monitorenter和monitorexit来隐式地使用这两个操作,这两个字节码指令反映到Java代码中就是同步块——synchronized关键字,因此在synchronized块之间的操作也具备原子性。java1.5后concurrent包还提供了一些原子变量类来实现原子性(CAS)。
可见性:一个线程对主内存的的修改可以及时的被其他线程观察到,Java语言能实现同步性可见性的有了volatile,synchronized和final。
有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序。Java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性
从上可以看出volatile只能保证可见性和有序性,并不能保证原子性。其中用来保证有序性和可见性的实现也和synchronized存在一定区别,volatile实现可见性和有序性是通过加入内存屏障来实现的,而synchronized则是通过一次控制一个线程执行(串行执行)来实现的,这点要注意区分下。
上面有提到内存屏障和指令重排序,可能有的朋友不了解,下面分别介绍下
内存屏障:为了保证内存可见性,Java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。
内存屏障类型表
指令重排序:重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。重排序分3种类型。
从源码到最终执行的指令序列的示意图
Java内存模型还规定了在执行上述8种基本操作时必须满足如下规则:
这8种内存访问操作以及上述规则限定,再加上稍后介绍的对volatile的一些特殊规定,就已经完全确定了Java程序中哪些内存访问操作在并发下是安全的。由于这种定义相当严谨但又十分烦琐,实践起来很麻烦,java语言中还有一个等效判断原则——先行发生原则(happens- before),他是判断数据是否存在竞争,线程是否安全的主要依据。
对于volatile型变量的特殊规则:
这里有一点需要注意:volatile保证可见性并不是说volatile直接操作主内存,volatile同样存在工作内存的拷贝,但是由于其特殊的操作顺序,所以看起来如同直接在主内存读写一般。
java内存模型涉及的知识点基本上是这些内容了,好好理解内存模型就能解决经常遇到的各种内存可见性问题。
原文链接:https://www.cnblogs.com/dailingnan/p/9435006.html