计算机中执行程序时,每条指令都是在CPU中执行,执行指令的过程必然会涉及到数据的读取和写入。而程序运行时的数据是存放在主存(物理内存)中,因为CPU的读写速度远远高于内存的速度,若是CPU直接和内存交互,会大大下降指令的执行速度,因此CPU里面就引入了高速缓存。html
脑补当初学习OS时的图 CPU->内存 CPU->寄存器->内存java
也就是说程序运行时,会将运算所须要的数据从主存中复制一份到高速缓存,CPU进行计算的时候能够直接从高速缓存读取和写入,当运算结束时,在将高速缓存中的数据刷新到主存。编程
可是若是那样必需要考虑,在多核CPU下数据的一致性问题怎么保证?好比i=i+1
,当线程执行这条时,会先从主存中读取i的值,而后复制一份到高速缓存,而后CPU执行指令对i进行加1操做,而后将数据写入高速缓存,最后将高速缓存中i最新的值刷新到主存当中。在单线程下这段代码运行不会存在问题,但若是在多线程下多核CPU中,每一个CPU都有本身的高速缓存,可能存在下面一种状况:初始时,两个线程分别读取i的值存入各自所在的CPU的高速缓存当中,而后线程1进行加1操做,而后把i的最新值1写入到内存。此时线程2的高速缓存当中i的值仍是0,进行加1操做以后,i的值为1,而后线程2把i的值写入内存。最终结果i的值是1,而不是2。这就是著名的缓存一致性问题。一般称这种被多个线程访问的变量为共享变量。缓存
为了解决缓存不一致性问题,一般来讲有如下2种解决方法:多线程
经过在总线加LOCK#锁的方式架构
经过缓存一致性协议并发
并发编程中,一般会考虑的三个问题原子性问题、可见性问题、有序性问题。app
(1)原子性:程序中的单步操做或多步操做要么所有执行而且执行的过程当中不能被打断,要么都不执行。性能
若是程序中不具有原子性会出现哪些问题?学习
转帐操做就是一个很好的表明,若是转帐的过程当中被中断,钱转出去了,因为中断,收帐方却没有收到。
(2)可见性:内存可见性,指的是线程之间的可见性,当一个线程修改了共享变量时,另外一个线程能够读取到这个修改后的值。
//线程1执行的代码 int i = 0; i = 10; //线程2执行的代码 j = i;
假若线程1从主存中读取了i的值并复制到CPU高速缓存,而后对i修改成10,这时CPU高速缓存中的i值为10,在没有将高速缓存中的值刷新到主存中时,线程2读取到的值仍是0,它看不到i值的变化,这就是可见性问题。
Java提供了Volatile关键字来保证可见性,当一个共享变量被volatile修饰时,它会保证修改的值会当即被更新到主存,当有其余线程须要读取时,它会去内存中读取新值。
而普通的共享变量不能保证可见性,由于普通共享变量被修改以后,何时被写入主存是不肯定的,当其余线程去读取时,此时内存中可能仍是原来的旧值,所以没法保证可见性。
另外,经过synchronized和Lock也可以保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁而后执行同步代码,而且在释放锁以前会将对变量的修改刷新到主存当中。所以能够保证可见性。
(3)有序性:程序执行的顺序按照代码的前后顺序执行。
实际是这样吗?
int i = 0; //[1] int a,b; //[2]
[2]必定会在[1]以后执行吗?不必定,在JVM中,有可能会发生指令重排序(Instruction Reorder)。若是[1]、[2]中有相互依赖,好比[2]中的数据依赖于[1]的结果,那么则不会发生指令重排序。
什么是指令重排序?
通常来讲,处理器为了提升程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行前后顺序同代码中的顺序一致,可是它会保证程序最终执行结果和代码顺序执行的结果是一致的。
指令重排对于提⾼CPU处理性能⼗分必要。虽然由此带来了乱序的问题,可是这点牺牲是值得的。
指令重排能够保证串⾏语义⼀致,可是没有义务保证多线程间的语义也⼀致。因此在多线程下,指令重排序可能会致使⼀些问题。
JVM能够看作是一个有OS架构的处理机,他也有本身的内存和处理器,它的内存和以前讨论的没有什么太大的差别。
Java运行时内存的划分以下:
对于每⼀个线程来讲,栈都是私有的,而堆是共有的。也就是说在栈中的变量(局部变量、⽅法定义参数、异常处理器参数)不会在线程之间共享,也就不会有内存可⻅性(下⽂会说到)的问题,也不受内存模型的影
响。⽽在堆中的变量是共享的,本⽂称为共享变量。因此内存可见性针对的是共享变量。
一、既然堆是共享的,为何在堆中会有内存不可⻅问题?
Java内存模型规定全部的变量都是存在主存当中(相似于前面说的物理内存),每一个线程都有本身的工做内存(相似于前面的高速缓存)。线程对变量的全部操做都必须在工做内存中进行,而不能直接对主存进行操做。而且每一个线程不能访问其余线程的工做内存。
线程之间的共享变量存在主内存中,每一个线程都有⼀个私有的本地内存,存储了该线程以读、写共享变量的副本。本地内存是Java内存模型的⼀个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器等。
Java线程之间的通讯由Java内存模型(简称JMM)控制,从抽象的⻆度来讲,JMM定义了线程和主内存之间的抽象关系。JMM的抽象示意图如图所示:
从图中能够看出:
二、JMM与Java内存区域划分的区别与联系
区别
JMM是抽象的,他是⽤来描述⼀组规则,经过这个规则来控制各个变量的访问⽅式,围绕原⼦性、有序性、可⻅性等展开的。⽽Java运⾏时内存的划分是具体的,是JVM运⾏Java程序时,必要的内存划分。
联系
都存在私有数据区域和共享数据区域。⼀般来讲,JMM中的主内存属于共享数据区域,他是包含了堆和⽅法区;一样,JMM中的本地内存属于私有数据区域,包含了程序计数器、本地⽅法栈、虚拟机栈。
原子性、可见性、有序性
Java内存模型具有一些先天的“有序性”,即不须要经过任何手段就可以获得保证的有序性,这个一般也称为 happens-before 原则。若是两个操做的执行次序没法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机能够随意地对它们进行重排序。
在Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model,JMM)来屏蔽各个硬件平台和操做系统的内存访问差别,以实现让Java程序在各类平台下都能达到一致的内存访问效果。那么Java内存模型规定了哪些东西呢,它定义了程序中变量的访问规则,往大一点说是定义了程序执行的次序。注意,为了得到较好的执行性能,Java内存模型并无限制执行引擎使用处理器的寄存器或者高速缓存来提高指令执行速度,也没有限制编译器对指令进行重排序。也就是说,在java内存模型中,也会存在缓存一致性问题和指令重排序的问题。
在Java中,volatile关键字有特殊的内存语义。volatile主要有如下两个功能:
内存可见性
所谓内存可见性,指的是当一个线程对volatile
修饰的变量进行过写操做时,JMM会当即把线程对应的本地内存中的共享变量的值刷新到主内存;当一个线程对volatile
修饰的变量进行读操做时,JMM会当即把该线程对应的本地内存置为无效,从内存中重新读取共享变量的值。
禁止重排序
JMM是经过内存屏障来限制处理器对指令的重排序的。
什么是内存屏障?硬件层面,内存屏障分两种:读屏障(Load Barrier)和写屏障(Store Barrier)。内存屏障有两个做用:
通俗说,经过内存屏障,能够防止指令重排序时,不会将屏障后面的指令排到以前,也不会将屏障以前的指令排到以后。
单例模式下的Double-Check(双重锁检查)
public class Singleton { public static Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) { //[1] synchronized (Singleton.class) { instance = new Singleton(); //[2] } } return instance; } }
若是这里的变量没有使用volatile关键字,那么有可能就会发生错误。
[2]实例化对象的过程能够分为分配内存、初始化对象、引用赋值。
instance = new Singleton(); // [1] // 能够分解为如下三个步骤 1 memory=allocate();// 分配内存 至关于c的malloc 2 ctorInstanc(memory) //初始化对象 3 s=memory //设置s指向刚分配的地址 // 上述三个步骤可能会被重排序为 1-3-2,也就是: 1 memory=allocate();// 分配内存 至关于c的malloc 3 s=memory //设置s指向刚分配的地址 2 ctorInstanc(memory) //初始化对象
若是一旦发生了上述的重排序,当程序执行了1和3,这时线程A执行了if判断,断定instance不为空,而后直接返回了一个未初始化的instance。