Java多线程高并发基础篇(五)-Java内存模型(JMM)
在虚拟机的规范中,定义了Java的内存模型(JMM),来屏蔽各种硬件和操作系统内存访问的差异。在JDK1.5(实现了JSR-133)发布后,Java内存模型就逐渐的完善起来了。
一. 并发编程要解决的关键问题
我们知道,JMM是围绕着并发过程中如何处理原子性,可见性,有序性这3个特征建立的。
1.原子性:有关原子性的内容请参考http://zhaodengfeng1989.iteye.com/blog/2418779
2.可见性:也就是线程之间以何种机制进行信息交互(也叫线程通信)。在命令式编程中,线程之间的通信的方式有两种:共享内存,消息传递。
①共享内存:在共享内存的并发模型中,线程之间共享内存的共享变量,通过线程之间写-读内存中的共享变量,进行线程之间的隐式通信。
②消息传递:在消息传递的并发模型里,线程之间没有公共状态,通过显式发送消息的方式进行显式通信。
3.有序性:也叫线程的同步,是指程序中用于控制线程间操作发生相对顺序的机制。
①在共享内存的并发模型中,需要显式的指定程序中某段代码需要互斥执行,因此线程之间的同步是显式进行的。
②在消息传递的并发模型中,由于消息发送和接收的相对顺序,线程之间的同步是隐式进行的。
Java的并发模型采用的是共享内存的方式,Java线程之间通过共享内存进行隐式通信,对外部完全透明,也就是我们今天看到Java内存模型。
二.Java内存模型的结构(也叫抽象结构)
1.线程之间如何通信
在虚拟机的体系结构中,我们知道堆内存和方法区是所有线程共享的。因此我们所说的共享内存模型,也建立在这两块区域上。因此,共享变量就是在共享内存中定义的变量,包括对象实例,静态域,数组元素。
我们来看一下JMM的抽象结构图:
从抽象出来的JMM结构看,JMM定义了线程与主内存(共享内存)之间的关系,每个线程都有一个私有的本地内存(Local Memory),在本地内存中存放着共享变量的副本。当然,线程的本地内存是JMM中抽象出来的一个概念,它涵盖了处理器的缓存,读写缓冲区,寄存器,一部分硬件以及编译器优化。
根据JMM的抽象结构,我们说下在共享内存的并发模型下,线程之间是如何通信的。
根据上图,我们可以知道,线程A,B之间要通信,必须要进行两步。
一是线程A把本地内存中更新过的共享变量副本刷新到主内存中;
二是线程B从主内存中读取线程A更新过的共享变量。
我们举一例详细说明。假设现在有两个线程,线程A,B,并且线程A,B的本地内存中都缓存了共享变量x,且x是初始状态值为0.
当线程A更新了共享变量x=1后,根据JMM的抽象结构,我们知道线程A,B之间要通信,那么需要做的第一步就是把更新后的x写入到主内存中,那么此时主内存的x=1。然后第二步线程B读取主内存中更新后的变量x=1,缓存在线程B的本地缓存中,那么此时,线程B中的共享变量x也是1(这里面涉及的机制参见帖子http://zhaodengfeng1989.iteye.com/blog/2418346)。过程见下图。
从以上可以看出,JMM通过控制主内存(共享内存)和线程本地内存的交互来进行线程之间的通信。
2.线程执行如何进行有序性操作?
从Java源代码到最后在处理器上实际执行的指令序列,会经历3种重排序。
①编译器优化重排序:在不改变单线程串行语义的前提下,可以重新安排语句的执行顺序。
②指令级并行重排序:现代处理器采用了指令级并行技术(ILP,Instruction-Level-Parallelism)来将多条指令重叠执行,如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序,从而形成指令流水线。
③内存系统的重排序:由于处理器使用缓存和写缓冲区,使得加载(读)和存储(写)操作看上去乱序执行。
上述①属于编译器重排序,②③属于处理器重排序。
我们一定要记住的是,
对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序。
对于处理器重排序,JMM的处理器重排序规则会要求在Java编译器生成指令序列时,插入特定类型的内存屏障指令(Memory Barrier/Fence),通过特定的内存屏障指令来禁止特定类型的处理器重排序。
下一帖我们重点说下重排序和先行发生(Happen-before)原则相关内容,他们是理解JMM的关键!!