pdf下载:
https://pan.baidu.com/s/1SM__fev_esbYhVOWo90RKw
java内存模型
解决内存可见行
问题。
并发编程处理两个问题:
1.线程之间如何通信
2.线程之间如何同步
通信:线程之间以何种机制来交换信息;
线程之间的通信机制有两种:
1.共享内存
2.消息传递
共享内存:
线程之间共享程序的公共状态,线程之间通过写-读内存中公共状态来隐式进行通信
消息传递:
线程之间没有公共状态,线程之间必须通过明确的发送消息显示进行通信
同步:程序用于控制不同线程之间操作执行相对顺序的机制;
在共享内存并发模型里,同步是显式进行的;
程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行。
在消息传递模型的并发编程里,由于消息的发送必须在消息的接收之前,因此同步是隐式进行的;
java的并发采用的是共享内存模型,java线程之间的通信总是隐式进行的。整个通信过程对程序员完全透明。
如果编写多线程的java程序员不理解隐式进行的线程之间通信的工作机制,很可能会遇到各种奇怪的内存可见性问题。
关键词:内存可见行
java内存模型的抽象
共享变量:实例域,静态域,数组元素
不会在线程共享:
局部变量,方法定义参数,异常处理器对象:
不会有内存可见行性问题,也不受内存模型影响。
jmm决定一个线程对共享变量写入何时对另一个线程可见。
共享主内存
线程私有内存
jmm通过控制主内存与每个线程的本地内存之间的交互,来为java程序员提供内存可见行保证。
重排序
编译器和处理器对指令重排序。
重排序分为三种:
编译器重排序
指令级并行的重排序
内存系统的重排序
这些重排序都可以导致多线程出现内存可见性
问题。
禁止重排序:jmm有重排序规则。处理器级别的有内存屏障
jmm(java内存模型)
是属于语言级的内存模型,它确保在不同的编译器和不同的处理器平台之上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见行
保证。
处理器重排序与内存屏障指令
从内存操作实际发生的顺序来看,直到处理器A执行A3来刷新自己的写缓存区,写操作A1才算真正执行了。虽然处理器A执行内存操作的顺序为:A1->A2,但内存操作实际发生的顺序却是:A2->A1。此时,处理器A的内存操作顺序被重排序了(处理器B的情况和处理器A一样,这里就不赘述了)。
这句话怎么理解?
对于处理器A来说,
a=1;//A1
x=b;//A2
是可以先执行A2再执行A1的,因为先执行A2再执行A1,对程序没有任何影响;a=1;和x=b;之间并没有逻辑上的先后因果关系。不存在数据依赖
写缓存仅对自己的处理器可见,它会导致处理器执行内存操作的顺序可能会与内存实际的操作执行顺序不一致。
x86仅仅允许对写-读操作做重排序。因为使用了写缓冲区。
为了保证内存可见性
,java编译器会在生成指令序列的适当位置插入内存屏障
来禁止特定类型的处理器重排序。
happens-before:用来阐述操作之间的内存可见行
从jdk5开始,java使用新的内存模型(jsr-133内存模型)
jsr-133使用hanppens-before的概念来阐述操作之间的内存可见行。
jmm中,如果一个操作执行的结果需要对另一操作可见,那么这两个操作之间必须要存在happens-before关系。
这里提到的两个操作,可以在一个线程之内,也可以在多个线程之间。
happens-before规则:
程序顺序规则:
一个线程内的所有操作,happens-before于该线程中的任意后续操作。
互斥锁规则:
对一个互斥锁的释放/解锁,happens-before于随后对这个互斥锁的获取/加锁。
volatile变量规则:
对一个volatile域的写,happens-before 于任意后续对这个volatile域的读。
传递性:
如果A happens-before B,且B happens-before C, 那么 A happens-before C
也就就说可以通过加锁,volatile修饰变量保证内存可见性
,
注意:两个操作之间具有happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行!
happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见
。
被volatile修饰的变量,在读之前,要先等待写并刷新的主内存,
互斥锁,在获取锁之间,必须等待锁的释放;
一个线程内,代码按顺序执行。
重排序
如果两个操作访问同一个变量,且这两个操作中有一个写操作,此时两个操作之间就存在了数据依赖性。
数据依赖性分下列三种类型:
写后读
写后写
读后写
上面三种情况,只要重排序两个操作的执行顺序,程序的执行结果将会被改变。
前面提到过,编译器和处理器可能会对操作做重排序。编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。
注意,这里所说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作,不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑。
as-if-serial语义
不管怎么重排序(编译器和处理器为了提高并发度),(单线程)程序的执行结果不能被改变。编译器,runtime,和处理器都必须遵循as-if-serial语义。
为了遵循as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序。因为这种重排序会改变执行结果。
但是,如果操作之间不存在数据依赖关系,这些操作被编译器和处理器重排序。
double pi = 3.14;//A
double r = 1.0;//B
double area = pirr;//C
A和C之间存在数据依赖关系,同时B和C之间也存在数据依赖关系。因此在最终执行的指令序列中,C不能被重排序到A和B的前面(C排到A和B的前面,程序的结果将会被改变)。
但A和B之间没有数据依赖关系,编译器和处理器可以重排序A和B之间的执行顺序。
实际上:执行顺序也可能是
double r = 1.0;//B
double pi = 3.14;//A
double area = pirr;//C
as-if-serial语义把单线程程序保护了起来,遵守as-if-serial语义的编译器,runtime 和处理器共同为编写单线程程序的程序员创建了一个幻觉:单线程程序是按程序的顺序来执行的。as-if-serial语义使单线程程序员无需担心重排序会干扰他们,也无需担心内存可见性问题。
程序顺序规则
在计算机中,软件技术和硬件技术有一个共同的目标:在不改变程序执行结果的前提下,尽可能的开发并行度。
编译器和处理器遵从这一目标,从happens- before的定义我们可以看出,JMM同样遵从这一目标。
重排序对多线程的影响
控制依赖关系
当代码中存在控制依赖性
时,会影响指令序列执行的并行度。
为此,编译器和处理器会采用猜测(Speculation)
执行来克服控制相关性对并行度的影响
。
在单线程程序中,对存在控制依赖的操作重排序,不会改变执行结果(这也是as-if-serial语义允许对存在控制依赖的操作做重排序的原因);但在多线程程序中,对存在控制依赖的操作重排序,可能会改变程序的执行结果。
数据竞争与顺序一致性保证
当程序未正确同步时,就会存在数据竞争。java内存模型规范对数据竞争的定义如下:
在一个线程中写一个变量,
在另一个线程读同一个变量,
而且写和读没有通过同步来排序。
jmm对正确同步的多线程程序的内存一致性
做了如下保证:
如果程序是正确同步的,程序的执行将具有顺序一致性
--即程序的执行结果与该程序在顺序一致性内存模型
中的执行结果相同,
这里的同步是指广义上的同步,包括对常用同步原语(lock,volatile和final)的正确使用。
顺序一致性内存模型
顺序一致性内存模型
是一个被计算机科学家理想化了的理论参考模型,
它为程序员提供了极强的内存可见性保证
。
顺序一致性内存模型有两大特性:
一个线程中所有操作必须按照程序的顺序来执行。
(不管程序是否同步)所有线程都只能看到一个单一的操作执行顺序。
在顺序一致性内存模型中,每个操作都必须原子执行且立即对所有线程可见。
举例说明:
顺序一致性
保证
未同步程序在顺序一致性模型中虽然整体执行顺序是无序的,但所有线程都只能看到一个一致的整体执行顺序。以上图为例,线程A和B看到的执行顺序都是:B1->A1->A2->B2->A3->B3。之所以能得到这个保证是因为顺序一致性内存模型中的每个操作必须立即对任意线程可见。
但是,jmm中就没有这个保证
。未同步程序在jmm中不但整体的执行顺序是无序的,而且所有线程看到的操作顺序也可能不一致。
比如,在当前线程把写过的数据缓存在本地内存中,且还没有刷新到主内存之前,这个写操作仅对当前线程可见;从其他线程的角度来观察,会认为这个写操作根本还没有被当前线程执行。只有当前线程把本地内存中写过的数据刷新到主内存之后,这个写操作才能对其他线程可见。在这种情况下,当前线程和其它线程看到的操作执行顺序将不一致。
同步程序顺序一致性效果
在顺序一致性模型中,所有操作完全按程序的顺序串行执行。而在JMM中,临界区内的代码可以重排序(但JMM不允许临界区内的代码“逸出”到临界区之外,那样会破坏监视器的语义)
。JMM会在退出监视器和进入监视器这两个关键时间点做一些特别处理,使得线程在这两个时间点具有与顺序一致性模型相同的内存视图(具体细节后文会说明)
。虽然线程A在临界区内做了重排序,但由于监视器的互斥执行的特性,这里的线程B根本无法“观察”到线程A在临界区内的重排序。这种重排序既提高了执行效率,又没有改变程序的执行结果。
从这里我们可以看到JMM在具体实现上的基本方针:在不改变(正确同步的)程序执行结果的前提下,尽可能的为编译器和处理器的优化打开方便之门。
未同步程序的执行特性
JMM不保证未同步程序的执行结果与该程序在顺序一致性模型中的执行结果一致。因为未同步程序在顺序一致性模型中执行时,整体上是无序的,其执行结果无法预知。保证未同步程序在两个模型中的执行结果一致毫无意义。
和顺序一致性模型一样,未同步程序在JMM中的执行时,整体上也是无序的,其执行结果也无法预知。同时,未同步程序在这两个模型中的执行特性有下面几个差异:
顺序一致性模型保证单线程内的操作会按程序的顺序执行,而JMM不保证单线程内的操作会按程序的顺序执行(比如上面正确同步的多线程程序在临界区内的重排序)。这一点前面已经讲过了,这里就不再赘述。
顺序一致性模型保证所有线程只能看到一致的操作执行顺序,而JMM不保证所有线程能看到一致的操作执行顺序。这一点前面也已经讲过,这里就不再赘述。
JMM不保证对64位的long型和double型变量的读/写操作具有原子性,而顺序一致性模型保证对所有的内存读/写操作都具有原子性。
线程之间的通信由java内存模型jmm控制,jmm决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,jmm定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存(local memory ),本地内存中存储了该线程以读/写共享变量的副本。本地内存是jmm中一个抽象概念,并不真实存在。它涵盖了缓存,写缓存区,寄存器以及其他的硬件和编译器优化,
volatile的特性
理解volatile特性的一个好方法是:把对volatile变量的单个读/写,看作是使用同一个monitor对这个单个读/写操作做了同步。
class VolatileFeaturesExample {
volatile long vl = 0L; //使用volatile声明64位的long型变量
public void set(long l) {
vl = l; //单个volatile变量的写
}
public void getAndIncrement () {
vl++; //复合(多个)volatile变量的读/写
}
public long get() {
return vl; //单个volatile变量的读
}
}
假设有多个线程分别调用上面程序的三个方法,这个程序在语意上和下面程序等价:
class VolatileFeaturesExample {
long vl = 0L; // 64位的long型普通变量
public synchronized void set(long l) { //对单个的普通 变量的写用同一个监视器同步
vl = l;
}
public void getAndIncrement () { //普通方法调用
long temp = get(); //调用已同步的读方法
temp += 1L; //普通写操作
set(temp); //调用已同步的写方法
}
public synchronized long get() {
//对单个的普通变量的读用同一个监视器同步
return vl;
}
}
监视器锁的happens-before规则保证释放监视器和获取监视器的两个线程之间的内存可见性,这意味着对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
监视器锁的语义决定了临界区代码的执行具有原子性。这意味着即使是64位的long型和double型变量,只要它是volatile变量,对该变量的读写就将具有原子性。如果是多个volatile操作或类似于volatile++这种复合操作,这些操作整体上不具有原子性。
简而言之,volatile变量自身具有下列特性:
可见行。对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
原子性:对任意单个volatile变量的读/写具有原子性。但类似于volatile++这种复合操作不具有原子性。
锁的释放--获取建立的happens-before关系
锁是java并发编程中最重要的同步机制。
锁除了让临界区互斥执行外,还可以让释放锁的线程向获取同一个锁的线程发送消息。
锁内存语义的具体实现机制
final
对于final域,编译器和处理器要遵守两个重排序规则:
1.在构造函数内对一个final域的写入,与随后把这个 被构造对象的引用(这个对象 指的是这个构造方法所在的类) 赋值给一个引用变量,这两个操作之间不能重排序。
2.初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。
写final域的重排序规则
写final域的重排序规则禁止把final域的写重排序到构造函数之外。这个规则的实现包含下面2个方面:
JMM禁止编译器把final域的写重排序到构造函数之外。
编译器会在final域的写之后,构造函数return之前,插入一个StoreStore屏障。这个屏障禁止处理器把final域的写重排序到构造函数之外。
写final域的重排序规则可以确保:在对象引用为任意线程可见之前,对象的final域已经被正确初始化过了,而普通域不具有这个保障。
这里final域为一个引用类型,它引用一个int型的数组对象。对于引用类型,写final域的重排序规则对编译器和处理器增加了如下约束:
在构造函数内对一个final引用的对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
处理器内存模型
顺序一致性内存模型
是一个理论参考模型
,jmm
和处理器内存模型
在设计时通常会把顺序一致性内存模型
作为参照。
jmm和处理器内存模型在设计时通常会把顺序一致性内存模型作为参照,
jmm和处理器内存模型在设计时会对顺序一致性模型做一些放松,因为如果完全按照顺序一致性模型来处理处理器和jmm,那么很多的处理器和编译器优化都要被禁止,这对执行性能将会有很多的影响。
根据对不同类型读/写操作组合的执行顺序的放松,可以把常见处理器内存模型划分为下面几种类型:
注意,这里处理器对读/写操作的放松,是以两个操作之间不存在数据依赖性为前提的
(因为处理器要遵守as-if-serial语义
,处理器不会对存在数据依赖性的两个内存操作做重排序
)。
jmm,处理器内存模型与顺序一致性内存模型之间的关系
jmm是个语言级的内存模型,
处理器内存模型是个硬件级的内存模型,
顺序一致内存模型是个理论参考模型。
因此,JMM把happens- before要求禁止的重排序分为了下面两类:
会改变程序执行结果的重排序。
不会改变程序执行结果的重排序。
JMM对这两种不同性质的重排序,采取了不同的策略:
对于会改变程序执行结果的重排序,JMM要求编译器和处理器必须禁止这种重排序。
对于不会改变程序执行结果的重排序,JMM对编译器和处理器不作要求(JMM允许这种重排序)。
jmm的内存可见性
保证
1.单线程程序。单线程程序不会出现内存可见行
问题。
编译器,runtime和处理器会共同确保单线程程序的执行结果与该程序在顺序一致性模型中的执行结果相同。
2.正确同步的多线程程序。
正确同步的多线程程序的执行将具有顺序一致性。
(程序的执行结果与该程序在顺序一致性模型中的执行结果相同)
这是jmm关注的重点,jmm通过限制编译器和处理器的重排序来为程序员提供内存可见性
的保证。
3.未同步/未正确同步的多线程程序
jmm为它们提供了最小安全性保障:
线程执行时读取到的值,要么是之前某个线程写入的值,要么是默认值(0,null,false)。
jsr-133对旧内存模型的修补
增强volatile的内存语义。
增强final的内存语义。