(Java Memory Model——Java内存模型)。什么是JMM呢?JMM是一个抽象概念,它并不存在。Java虚拟机规范中试图定义一种Java内存模型(JMM)来屏蔽掉各类硬件和操做系统的内存访问差别,以实现让Java程序在各类平台下都能达到一致的内存访问效果。在此以前,主流程序语言(如C/C++等)直接使用物理硬件和操做系统的内存模型,所以,会因为不一样平台的内存模型的差别,有可能致使程序在一套平台上并发彻底正常,而在另外一套平台上并发访问却常常出错,所以在某些场景就必须针对不一样的平台来编写程序。html
之间的通讯由JMM来控制,JMM决定一个线程共享变量的写入什么时候对另外一个线程可见。JMM保证若是程序是正确同步的,那么程序的执行将具备顺序一致性。从抽象的角度看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量(实例域、静态域和数据元素)存储在主内存(
Main Memory
)中,每一个线程都有一个私有的本地内存(Local Memory
),本地内存中存储了该线程以读/写共享变量的副本(局部变量、方法定义参数和异常处理参数是不会在线程之间共享,它们存储在线程的本地内存中)。从物理角度上看,主内存仅仅是虚拟机内存的一部分,与物理硬件的主内存名字同样,二者能够互相类比;而本地内存,可与处理器高速缓存类比。Java内存模型的抽象示意图如图所示: 程序员
关于主内存与本地内存之间具体的交互协议,即一个变量如何从主内存拷贝到本地内存、如何从本地内存同步回主内存之类的实现细节,JMM中定义了如下8种操做来完成,虚拟机实现时必须保证下面说起的每种操做都是原子的、不可再分的(对于double和long类型的遍从来说,load、store、read和write操做在某些平台上容许有例外):面试
若是要把一个变量从主内存模型复制到本地内存,那就要顺序的执行read和load操做,若是要把变量从本地内存同步回主内存,就要顺序的执行store和write操做。注意,Java内存模型只要求上述两个操做必须按顺序执行,而没有保证是连续执行。也就是说read与load之间、store与write之间是可插入其余指令的,如对主内存中的变量a、b进行访问时,一种可能出现的顺序是read a read b、load b、load a。编程
内存屏障是一组处理器指令(前面的8个操做指令),用于实现对内存操做的顺序限制。包括LoadLoad, LoadStore, StoreLoad, StoreStore共4种内存屏障。内存屏障存在的意义是什么呢?它是在Java编译器生成指令序列的适当位置插入内存屏障指令来禁止特定类型的处理器重排序,从而让程序按咱们预想的流程去执行,内存屏障是与相应的内存重排序相对应的。JMM把内存屏障指令分为4类:缓存
若是两个操做访问同一个变量,且这两个操做中有一个为写操做,此时这两个操做之间就存在数据依赖性。数据依赖性分3种类型:写后读、写后写、读后写。这3种状况,只要重排序两个操做的执行顺序,程序的执行结果就会被改变。编译器和处理器可能对操做进行重排序。而它们进行重排序时,会遵照数据依赖性,不会改变数据依赖关系的两个操做的执行顺序。bash
名称 | 代码 | 示例说明 |
---|---|---|
写后读 | a = 1;b = a; | 写一个变量以后,再读这个位置。 |
写后写 | a = 1;a = 2; | 写一个变量以后,再写这个变量。 |
读后写 | a = b;b = 1; | 读一个变量以后,再写这个变量。 |
这里所说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操做,不一样处理器之间和不一样线程之间的数据依赖性不被编译器和处理器考虑。多线程
顺序一致性内存模型是一个理论参考模型,在设计的时候,处理器的内存模型和编程语言的内存模型都会以顺序一致性内存模型做为参照。它有两个特性:并发
从顺序一致性模型中,咱们能够知道程序全部操做彻底按照程序的顺序串行执行。而在JMM中,临界区内的代码能够重排序(但JMM不容许临界区内的代码“逸出”到临界区外,那样就破坏监视器的语义)。app
假设这两个线程使用监视器锁来正确同步:A线程的3个操做执行后释放监视器锁,随后B线程获取同一个监视器锁。 编程语言
假设这两个线程没有作同步: ![]()
![]()
JMM会在退出临界区和进入临界区这两个关键时间点作一些特别处理,使得线程在这两个时间点具备与顺序一致性模型相同的内存视图。虽然线程A在临界区内作了重排序,但因为监视器互斥执行的特性,这里的线程B根本没法“观察”到线程A在临界区内的重排序。这种重排序既提升了执行效率,又没有改变程序的执行结果。像单例模型[静态内部类模型]的类初始化解决方案就是采用了这个思想。
as-if-serial的意思是无论怎么重排序,(单线程)程序的执行结果不能改变。编译器、runtime和处理器都必须遵照as-if-serial语义。为了遵照as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操做作重排序。
as-if-serial语义把单线程程序保护了起来,遵照as-if-serial语义的编译器、runtime和处理器共同为编写单线程程序的程序员建立了一个幻觉:单线程程序是按程序的顺序来执行的。as-if-serial语义使单线程程序员无需担忧重排序会干扰他们,也无需担忧内存可见性问题。
happens-before是JMM最核心的概念。从JDK5开始,Java使用新的JSR-133内存模型,JSR-133 使用happens-before的概念阐述操做之间的内存可见性,若是一个操做执行的结果须要对另外一个操做可见,那么这两个操做之间必须存在happens-before关系。
happens-before规则以下:
终于谈到咱们反复说起的重排序了,重排序是指编译器和处理器为了优化程序性能而对指令序列进行从新排序的一种手段。重排序分3种类型。
JMM属于语言级的内存模型,它确保在不一样的编译器和不一样的处理器平台之上,经过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。
从JMM设计者的角度来讲,在设计JMM时,须要考虑两个关键因素:
JMM设计就须要在这二者之间做出协调。JMM对程序采起了不一样的策略:
介绍完了这几个基本概念,咱们不难推断出JMM是围绕着在并发过程当中如何处理原子性、可见性和有序性这三个特征来创建的。
经过前面8个操做指令和happens-before原则介绍,也不难推断出,volatile和synchronized两个关键字来保证线程之间的有序性,volatile自己就包含了禁止指令重排序的语义,而synchronized则是由监视器法则得到。
也许你对volatile和CAS的底层实现原理不是很了解,这里简单介绍下它们的底层实现:
Java语言规范第三版对volatile的定义为:Java编程语言容许线程访问共享变量,为了确保共享变量能被准确和一致性的更新,线程应该确保经过排他锁单独得到这个变量。若是一个字段被声明为volatile,Java内存模型确保这个全部线程看到这个值的变量是一致的。
而volatile是如何来保证可见性的呢?若是对声明了volatile的变量进行写操做,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存(Lock指令会在声言该信号期间锁总线/缓存,这样就独占了系统内存)。
可是,就算是写回到内存,若是其余处理器缓存的值仍是旧的,再执行计算操做就会有问题。因此,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每一个处理器经过嗅探在总线(注意处理器不直接跟系统内存交互,而是经过总线)上传播的数据来检查本身缓存的值是否是过时了,当处理器发现直接缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操做的时候,会从新从系统内存中把数据读处处理器缓存里。
CAS其实应用挺普遍的,咱们经常听到的悲观锁乐观锁的概念,乐观锁(无锁)指的就是CAS。
这里只是简单说下在并发的应用,所谓的乐观并发策略,通俗的说,就是先进性操做,若是没有其余线程争用共享数据,那操做就成功了,若是共享数据有争用,产生了冲突,那就采起其余的补偿措施(最多见的补偿措施就是不断重试,治到成功为止,这里其实也就是自旋CAS的概念),这种乐观的并发策略的许多实现都不须要把线程挂起,所以这种操做也被称为非阻塞同步。而CAS这种乐观并发策略操做和冲突检测这两个步骤具有的原子性,是靠什么保证的呢?硬件,硬件保证了一个从语义上看起来须要屡次操做的行为只经过一条处理器指令就能完成。
也许你会存在疑问,为何这种无锁的方案通常会比直接加锁效率更高呢?这里其实涉及到线程的实现和线程的状态转换。实现线程主要有三种方式:使用内核线程实现、使用用户线程实现和使用用户线程加轻量级进程混合实现。而Java的线程实现则依赖于平台使用的线程模型。至于状态转换,Java定义了6种线程状态,在任意一个时间点,一个线程只能有且只有其中的一种状态,这6种状态分别是:新建、运行、无限期等待、限期等待、阻塞、结束。
Java的线程是映射到操做系统的原生线程之上的,若是要阻塞或唤醒一个线程,都须要操做系统来帮忙完成,这就须要从用户态转换到核心态中,所以状态转换须要耗费不少的处理器时间。对于简单的同步块(被synchronized修饰的方法),状态转换消耗的时间可能比用户代码执行的时间还要长。因此出现了这种优化方案,在操做系统阻塞线程之间引入一段自旋过程或一直自旋直到成功为止。避免频繁的切入到核心态之中。 可是这种方案其实也并不完美,在这里就说下CAS实现原子操做的三大问题:
在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操做之间不能重排序。 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操做之间不能重排序。
增长了以下规则:在构造函数内对一个final引用的对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操做之间不能重排序。
一道面试题: [不使用volatile怎么打破循环?]
public class TestThread implements Serializable {
public static void main(String[] args) throws InterruptedException {
Data data = new Data();
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
data.add();
}).start();
while (data.num == 0) {
//怎么打破 死循环
}
/**-----------------------无责任分割线1-----------------------------------------------*/
int i = 1;
while (data.num == 0) {
i = i++; //未触发致使死循环
i = ++i;
}
/**-----------------------无责任分割线2-----------------------------------------------*/
while (data.num == 0) {
synchronized (TestThread.class) {
//同步锁触发线程切换 跳出循环
}
}
/**-----------------------无责任分割线3-----------------------------------------------*/
while (data.num == 0) {
Thread.yield();//线程让步 跳出循环
}
/**-----------------------无责任分割线4-----------------------------------------------*/
while (data.num == 0) {
try {
Thread.sleep(0);//线程休眠让出CPU 跳出循环
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**-----------------------无责任分割线5-----------------------------------------------*/
while (data.num == 0) {
System.out.println("");//println 有同步锁 跳出循环
}
/**-----------------------无责任分割线6-----------------------------------------------*/
LongAdder longAdder = new LongAdder();
while (data.num == 0) {
longAdder.decrement();//cas自旋锁 跳出循环
}
/**-----------------------无责任分割线7-----------------------------------------------*/
System.out.println("哈哈2");
}
static class Data {
volatile int num = 0;
public void add() {
this.num = 60;
}
}
}
复制代码
本文摘(jie)抄(jian)自 鸣谢原文:从一个简单的Java单例示例谈谈并发 JMM JUC