Java内存模型
说起Java内存模型(JMM)就涉及到Java的并发编程。在并发编程中,我们主要面对两个关键的问题,就是同步与通信。如何同步?通常我们会通过加锁来控制不同线程之间的执行顺序,从而达到同步的效果;线程间如何通信?线程间的通信机制有两种:共享内存与消息传递。Java的并发采用的是共享内存模型,如下图,两个线程通过JMM的控制完成了相互通信。通过图中也可以看出两个线程的共享变量存储在主内存中,每个线程同时又有自己的存储空间来存储共享变量的副本。
那么,当两个线程需要通信是是怎样的一个过程?也就是当线程A操作其中一个共享变量副本后,需要将更新过后的值刷新到主存中,然后线程B通过从主存中读取相应的值来实现相互通信。其间的详细过程就是有JMM控制的。(其中线程A什么时候讲共享变量副本刷新到主内存的时间是不确定的,但是当我们使用同步原语是(volatile,synchronized),刷新的时间将会确定)
Java内存模型中的重排序:
在JMM中,重排序指的是为了提高性能,编译器或处理器会对指令执行顺序进行重新排列。
重排序分为3中类型:
1.编译器优化的重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
2.指令级并行的重排序:现代处理器采用指令级并行技术将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应指令的执行顺序。
3.内存系统的重排序:由于处理器使用缓存和读/写缓冲区,是的加载和存储操作看上去可能是乱序执行。
虽然在指令在执行的过程中可能会被重排序,但一个基本坚持的原则是保证数据读/写处理的前后一致性,即具有数据依赖性的时候要保证数据的变化对所有使用数据着可见,并且保证数据的有效性。
所以在保证可见性的前提下,Java编译器就会在指令序列适当的位置加入内存屏障来禁止特定类型的处理器重排序。
JMM内存屏障有4种:
LoadLoadBarriers:指令序列:Load1;LoadLoad;Load2 确保Load1数据的装载先于Load2及所有后续转载指令的装载
StoreStoreBarriers:指令序列:Store1;StoreStore;Store2 确保Store1数据对其他处理器可见(刷新dao到内存)先于Store2及所有后续存储指令的存储
LoadStoreBarriers:指令序列:Load1;LoadStore;Store2 确保Load1数据装载先于Store2以及后续存储指令刷新到内存
StoreLoadBarriers:指令序列:Store1;StoreLoad;Load2 确保Store1数据对其他处理器可见先于Load2及后续装载指令的装载。StoreLoadBarriers会使该屏障之前所有内存访问指令完成之后,才执行该屏障之后的内存访问指令。
happens-before规则:
从字面上理解,就是一个操作先于另外一个操作。JMM规定,当一个操作的结果对另外一个操作可见时,着两个操作必须存在happens-before关系。
规则一:一个线程中的每个操作,happens-before与该线程的任意后续操作。
规则二:对于一个锁的解锁,happens-before于随后对这个锁的加锁。
规则三:对一个volatile变量的写,happens-before与任意后续对这个volatile域的读。
规则四:如果A happens-before B,B happens-before C,则A happens-before C,即传递性。
happens-before是JMM中关键的概念,有了happens-before规则,便确定了操作之间的相互顺序,也就可以约束不同线程间操作的同步关系。
顺序一致性内存模型:
顺序一致性内存模型是一个理论参考模型,我们可以这么理解,当程序正确同步时,程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同,也就是程序会按照我们设想中的顺序执行,就像不同的线程同步执行结果和串行执行的结果一样。
这里先将一个概念,数据竞争:在一个线程中写一个变量,在另外一个线程读同一个变量,而且写和读没有通过同步来排序,那么这两个线程是存在数据竞争的。所以当两个线程存在数据竞争时,如果未正确同步,就可能产生不一样的执行结果。
在顺序一致性内存模型中,一个线程所有的操作必须按照程序的顺序来执行。所有的线程只能看到一个单一的操作执行顺序,也就是每个操作都必须原子执行且立刻对所有线程可见。顺序一致性模型只是一个参考,如果对程序不加控制,执行结果依然达不到预期。
volatile内存语义:
volatile主要由于变量,当声明共享变量为volatile时,我们可以理解为对这个共享变量加了一个锁进行同步。虽然volatile相对于synchronized的开销较低,但是volatile的同步性较差。在使用volatile的时候我们需要注意一些问题,也就是声明为volatile变量的操作不具备原子性,当变量的值与当前值相关时(比如 i = i + 1),这个时候变量的操作是没有原子性的。
那么volatile的内存语义具体是什么呢,对于volatile写来说,当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存;对于volatile的读来说,当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,线程接下来姜葱主内存中读取共享变量。
锁的内存语义:
锁是Java中重要的同步机制,锁可以让临街区互斥执行,还可以让释放锁的线程向获取锁的线程发送消息。
锁的内存语义就可以总结为,当线程释放锁时,JMM会把该线程中的共享变量刷新到主内存中。当线程获取锁时,JMM会把该线程对应的本地内存置为无效,是的被监视器保护的临界区代码必须从主内存中获取共享变量。
final域的内存语义:
对于final语义的解释主要是在读final域之前,final与已经被初始化过了。
实现final语义主要是定义final域的重排序规则,写final域的重排序规则:1.JMM机制编译器把final域的写重排序到构造函数外;2.编译器会在final域写之后,构造函数return之前,插入一个storestore屏障,这个屏障禁止处理器重排序吧final域的写重排序到构造函数之外。