Java 内存模型(JMM)是一种抽象的概念,并不真实存在,它描述了一组规则或规范,经过这组规范定义了程序中各个变量(包括实例字段、静态字段和构成数组对象的元素)的访问方式。试图屏蔽各类硬件和操做系统的内存访问差别,以实现让 Java 程序在各类平台下都能达到一致的内存访问效果。程序员
注意JMM与JVM内存区域划分的区别:数组
处理器上的寄存器的读写的速度比内存快几个数量级,为了解决这种速度矛盾,在它们之间加入了高速缓存。缓存
加入高速缓存带来了一个新的问题:缓存一致性。若是多个缓存共享同一块主内存区域,那么多个缓存的数据可能会不一致,须要一些协议来解决这个问题。安全
全部的变量都 存储在主内存中,每一个线程还有本身的工做内存 ,工做内存存储在高速缓存或者寄存器中,保存了该线程使用的变量的主内存副本拷贝。多线程
线程只能直接操做工做内存中的变量,不一样线程之间的变量值传递须要经过主内存来完成。架构
Java 内存模型定义了 8 个操做来完成主内存和工做内存的交互操做。并发
Java 内存模型保证了 read、load、use、assign、store、write、lock 和 unlock 操做具备原子性,例如对一个 int 类型的变量执行 assign 赋值操做,这个操做就是原子性的。可是 Java 内存模型容许虚拟机将没有被 volatile 修饰的 64 位数据(long,double)的读写操做划分为两次 32 位的操做来进行,即 load、store、read 和 write 操做能够不具有原子性。app
有一个错误认识就是,int 等原子性的类型在多线程环境中不会出现线程安全问题。前面的线程不安全示例代码中,cnt 属于 int 类型变量,1000 个线程对它进行自增操做以后,获得的值为 997 而不是 1000。分布式
为了方便讨论,将内存间的交互操做简化为 3 个:load、assign、store。函数
下图演示了两个线程同时对 cnt 进行操做,load、assign、store 这一系列操做总体上看不具有原子性,那么在 T1 修改 cnt 而且尚未将修改后的值写入主内存,T2 依然能够读入旧值。能够看出,这两个线程虽然执行了两次自增运算,可是主内存中 cnt 的值最后为 1 而不是 2。所以对 int 类型读写操做知足原子性只是说明 load、assign、store 这些单个操做具有原子性。
AtomicInteger 能保证多个线程修改的原子性。
使用 AtomicInteger 重写以前线程不安全的代码以后获得如下线程安全实现:
public class AtomicExample { private AtomicInteger cnt = new AtomicInteger(); public void add() { cnt.incrementAndGet(); } public int get() { return cnt.get(); } }复制代码
public static void main(String[] args) throws InterruptedException { final int threadSize = 1000; AtomicExample example = new AtomicExample(); // 只修改这条语句 final CountDownLatch countDownLatch = new CountDownLatch(threadSize); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < threadSize; i++) { executorService.execute(() -> { example.add(); countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); System.out.println(example.get()); }复制代码
1000复制代码
除了使用原子类以外,也可使用 synchronized 互斥锁来保证操做的原子性。它对应的内存间交互操做为:lock 和 unlock,在虚拟机实现上对应的字节码指令为 monitorenter 和 monitorexit。
public class AtomicSynchronizedExample { private int cnt = 0; public synchronized void add() { cnt++; } public synchronized int get() { return cnt; } }复制代码
public static void main(String[] args) throws InterruptedException { final int threadSize = 1000; AtomicSynchronizedExample example = new AtomicSynchronizedExample(); final CountDownLatch countDownLatch = new CountDownLatch(threadSize); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < threadSize; i++) { executorService.execute(() -> { example.add(); countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); System.out.println(example.get()); }复制代码
1000复制代码
可见性指当一个线程修改了共享变量的值,其它线程可以当即得知这个修改。Java 内存模型是经过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值来实现可见性的。JMM 内部的实现一般是依赖于所谓的 内存屏障 ,经过 禁止某些重排序 的方式,提供内存 可见性保证 ,也就是实现了 各类 happen-before 规则 。与此同时,更多复杂度在于,须要尽可能确保各类编译器、各类体系结构的处理器,都可以提供一致的行为。
主要有有三种实现可见性的方式:
对前面的线程不安全示例中的 cnt 变量使用 volatile 修饰,不能解决线程不安全问题,由于 volatile 并不能保证操做的原子性。
有序性是指:在本线程内观察,全部操做都是有序的。在一个线程观察另外一个线程,全部操做都是无序的,无序是由于发生了指令重排序。在 Java 内存模型中,容许编译器和处理器对指令进行重排序,重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
volatile 关键字经过添加内存屏障的方式来禁止指令重排,即重排序时不能把后面的指令放到内存屏障以前。
也能够经过 synchronized 来保证有序性,它保证每一个时刻只有一个线程执行同步代码,至关因而让线程顺序执行同步代码。
JSR-133内存模型使用先行发生原则在Java内存模型中保证多线程操做 可见性 的机制,也是对早期语言规范中含糊的可见性概念的一个精肯定义。上面提到了能够用 volatile 和 synchronized 来保证有序性。除此以外,JVM 还规定了先行发生原则,让一个操做 无需控制 就能先于另外一个操做完成。
因为 指令重排序 的存在,两个操做之间有happen-before关系, 并不意味着前一个操做必需要在后一个操做以前执行。 仅仅要求前一个操做的执行结果对于后一个操做是可见的,而且前一个操做 按顺序 排在第二个操做以前。
Single Thread rule
在一个线程内,在程序前面的操做先行发生于后面的操做。
Monitor Lock Rule
一个 unlock(解锁) 操做 先行发生于 后面对同一个锁的 lock(加锁) 操做。
Volatile Variable Rule
对一个 volatile 变量的 写操做 先行发生于后面对这个变量的 读操做 。
Thread Start Rule
Thread 对象的 start() 方法调用先行发生于此线程的每个动做。
Thread Join Rule
Thread 对象的结束先行发生于 join() 方法返回。
Thread Interruption Rule
对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,能够经过 interrupted() 方法检测到是否有中断发生。
Finalizer Rule
一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始。
Transitivity
若是操做 A 先行发生于操做 B,操做 B 先行发生于操做 C,那么操做 A 先行发生于操做 C。