在多核处理器系统中,处理器一般有一级或者多级的内部缓存(CPU参数中常常看到的L1,L2,L3就是),他们既提升了访问数据的性能(由于数据更接近处理器而不用受内存速度的影响),同时也减小了在共享内存总线时的冲突(由于不少状况下内部缓存就以及缓存了内存的操做)。 处理器缓存能够很是明显的改善性能,但同时它也带来了一个新的挑战。 好比说,当有多个处理器(对于多核处理器就是一个核心)同时访问同一个内存地址的时候,在什么条件下他们看见的是同一个值呢? 由于不一样的处理器可能都有本身的缓存, 如何保证多个处理器都必须从内存中去读取该内存地址的数据呢?html
在处理器层次,一个内存模型为以下问题定义了必要和高效的条件:java
*当前处理器如何“看见”其余处理器对内存地址的“写”操做 *其余处理器如何“看见”当前处理器对内存地址的“写”操做
一些处理器提出了一种强内存模型,它要求全部处理器在任什么时候刻对任何内存地址都看到相同的值, 另一些处理器提出了一种弱内存模型,其实也就是一种特殊的指令: 内存屏障(memory barriers
),为了当前处理器看见其余处理器对内存的写或者其余处理器看见当前处理器的操做它要求刷新或者验证处理器缓存的数据。 在加锁(lock
)或者解锁(unlock
)的时候内存屏障常常发生。 可是对于高级语言来讲它是不可见的。编程
因为内存屏障的缺失,强内存模型在某些状况下更容易编程。可是,即便在强内存模型下, 内存屏障也常常是必须的。 他们的位置常常是违反直觉的。 最近的处理器设计中,因为内存屏障对于贯穿多个处理器和大内存之间的内存一致性作出的保证,更倾向于弱内存模型数组
因为编译器重排序,线程对内存地址的写操做是否对其余线程也可见这个问题变得更加复杂。 编译器可能会认为移动某个写操做在后面是更高效的(这里涉及到编译器的优化策略, 典型的是循环体中不会改变的变量做为循环条件,而后在其余线程修改,可是根据语义分析,编译器会以为循环条件不会改变而把变量移动位置 ),固然,这些操做都不会改变程序的语义。 因此若是编译器延迟或者提早了某个操做,这会很明显的影响其余线程看到的数据。 这些折射出了缓存缓存的影响。缓存
更广泛的是,程序中的写操做会被向前移动。 这种状况下,其余线程可能会看到一个尚未真正发生的“写”操做。 这些灵活性都是特地设计的: 给予编译器、运行环境或者硬件在最优的顺序执行代码的灵活性。在内存模型的范畴内,咱们能够达到更高的性能。
考虑以下代码:安全
ClassReordering{
int x =0, y =0;
publicvoid write(){
x =1;
y =2;
}
publicvoid reader(){
int r1 = y ;
int r2 = x;
}
}
多线程状况下,上述代码中的reader将会看到y=2 ,由于y在x以后被赋值。编写代码的可能认为x的值必定是1,可是,赋值操做极可能被重排序。 好比说 ,对y的赋值可能先于对x的赋值。 此时,若是对y的赋值完成事后,线程就调用了reader方法,那么r1将会是2,r2倒是0 。 因为重排序带来的不肯定性,r1和r2的值彻底没法肯定。多线程
Java内存模型描述了多线程环境下哪一种代码是合法的,线程和内存是如何相互影响。它描述了低层次的存储细节和程序变量、从真实系统中内存或者寄存器读或者写数据的关系。经过多种硬件以及多种编译器优化来实现该模型。
Java包含多种语言指令,好比 volatile, final ,synchronized
, 他们向编译器描述了并发程序的要求。 Java内存模型定义了volatile
和synchronized
的行为,最重要的,它肯定在全部多种处理器平台上具备相同的行为。 也就是说,java内存模型抽象了处理器相关的并发程序处理细节,提供了一个与具体平台无关的、语义确保获得保证的并发抽象层。 并发
C以及C++语言的多线程程序都是与具体编译器、操做系统、处理器强相关的。 不一样平台下的代码不兼容。oracle
1997年开始,在java语言规范的17章就有了java内存模型的定义。 它定义了一些看起来是使人困惑的行为(好比final字段可能看起来改变了值)。 它也阻碍了编译器进行通用优化。app
Java内存模型是一个充满雄心壮志的愿景。它是第一次有语言规范提出能够保证在并发在多种处理器之间有相同语义的内存模型。不幸的是,实现起来比想象中的还要困难。 JSR133提出了一个修复了以前的问题的一个新的内存模型。同时,改变了final和volatile 的语义。
JSR133的目标包括:
synchronization
)。有许多程序变量(类实例变量、类静态变量、数组)并无按照代码中指定的顺序运行的例子,编译器为了优化能够自由选择指令顺序(语义一致的状况),处理器也可能乱序执行指令。数据可能按照彻底不一样代码中的顺序在处理器、处理器缓存、内存中移动。
好比说, 若是某个线程中先对变量a赋值,而后对变量b赋值, b的赋值不依赖于a的状况下,编译器能够自由改变他们的顺序。处理器缓存也可能在a以前就把b的值刷新到内存中去。 有许多可能的重排序源,好比编译器,JIT,CPU缓存。在现代处理器中,乱序执行、分支预测等手段都会致使这个问题。
编译器、运行时环境、硬件协同构造了一种“顺序执行”的假象,这意味着在单线程程序中程序是彻底不会受到重排序的影响, 由于代码真的像是顺序执行下来的。 可是没有正确使用同步的多线程状况下,线程之间是否能看见相同的值或者看到改变彻底是随机性的。 由于重排序使得每一个线程可能都不是按照代码中的顺序执行的, 线程彼此没有交互的话极可能看到的不是相同的值。
大多数状况下,一个线程不关心其余线程作什么,若是关心就是synchronization
所作的事了。
incorrectly synchronization
)?一般状况下,以下的代码会被认为是不正确的同步:
data race
),程序也就是一个没有正确同步的程序同步有多个方面:最为人所知的互斥性(mutex
): 即同一时间只有一个线程拥有某个对象的监视器(monitor
),也意味着一旦一个线程进入了监视器(monitor
)对象的同步代码快中,访问同一对象的同步代码块的其余线程必须等到拥有监视器(monitor
)的线程退出同步代码库才行。
可是,synchronization
还有一个一个比互斥更重要的特性:同步确保一个线程的内存写操做对其余拥有相同监视器(monitor
)的线程可见。当咱们推出同步代码块时,就释放了该监视器,它将会把处理器缓存中的数据刷新到内存中,以便于其余线程能够看到该线程所作的更改。在咱们进入一个同步代码块以前,咱们会申请一个监视器(monitor
),这一步骤会使得当前处理器的缓存失效而不得不从内存中从新读取数据,这样咱们就能够看见任意线程所作的任何更改了。
新的内存模型语义在内存操做(read field, write field , lock ,unlock
)、线程操做(start , join
)中创造了一个非公平顺序,即一些操做会发生在另一些操做以前(happen-before
),当一个操做在另外一个操做以前时,前面一个将会确保在后面一个的前面而且是可见的。 排序的规则以下:
happens-before
以后的操做unlocl
是happens-before
接下来的lock
的volatile
变量的写操做happens-before
读操做start()
方法happens-before
调用它的线程的任意操做happens-before
该线程中join()
方法返回的全部线程 monitor
),当前同步块中的内存操做对于以后进入同步代码的任何线程都是可见的,也就是全部的内存操做happens-before
于监视器的释放,监视器的释放happens-before
监视器的获取。 synchronization
是reentrant
的,也就是同步代码块能够调用同一个监视器的中的其余同步代码块, 也就是说拥有屡次lock, 可是其实是同一个锁
Recall that a thread cannot acquire a lock owned by another thread. But a thread can acquire a lock that it already owns. Allowing a thread to acquire the same lock more than once enables reentrant synchronization. This describes a situation where synchronized code, directly or indirectly, invokes a method that also contains synchronized code, and both sets of code use the same lock. Without reentrant synchronization, synchronized code would have to take many additional precautions to avoid having a thread cause itself to block.
locksync
为了创建happens-before
关系,线程都应该是在相同的监视器(monitor
)上。线程A在对象X的同步代码块不是happens-before
以后线程B在对象Y的同步代码块。释放和申请锁必须一一对应,不然,就是一个data race
final
在新的内存模型(从JDK1.5开始的)是如何工做的?对象的final属性在构造函数中设置,一旦一个对象被正确的构造,给final属性赋的值对于其余全部线程都是可见的了,无论是否在同步块中。 另外,这个final属性所应用的任何对象或者数组也和final属性自己同样对全部线程都是最新的。 这意味这对于final属性,不须要额外的同步代码便可保证其余线程也能够看见最新的值。
那么什么是“对象被正确的构造”?
简单的说,这意味着该对象的this引用没有“溢出”构造函数中,否则其余线程可能经过this引用访问到“初始化一半”了的对象。好比说,不要赋给静态域、也不要把其余对象做为一个回调等等。这些能够在构造函数完成事后作而不是构造函数中。
classFinalFieldExample{
finalint x;
int y;
staticFinalFieldExample f;
publicFinalFieldExample(){
x =3;
y =4;
}
staticvoid writer(){
f =newFinalFieldExample();
}
staticvoid reader(){
if(f !=null){
int i = f.x;
int j = f.y;
}
}
}
上面代码描述了如何使用final,调用reader方法的线程必定会看到x的值为3,由于x是final的。而y则不保证必定是4. 若是上述代码的构造函数是这样:
publicFinalFieldExample(){// bad!
x =3;
y =4;
//this溢出,应该避免这样的代码
global.obj =this;
}
那么线程将能够经过global.obj看到this的引用,x的值就不能保证是3.
对于final属性能够看到正确的构造函数中赋的值是很是好的特性,若是final属性自己是一个引用,那么在其余任何线程均可以保证final属性“至少”会看到构造函数中所指定的值,而若是某个线程经过该引用的方法修改了数据, 则不保证全部线程都能看到该值, 为了确保全部线程均可以看到最新的值,你仍是必须使用synchronization
来同步。
final属性能够保证能看到对象构造函数值中最后指定的值, 而不是最新的值。
使用JNI来改变final值是未定义行为
若是final是引用或者数组类型,仍然须要同步synchronization
来保证全部线程均可以看见最新的值
volatile
volatile
是一个一般用于线程通讯的特殊字段,每次对volatile
变量的读都会读取全部线程中最新的值。 因此,它一般被设计为多线程之间的一个标志性变量,它的值可能会不停的改变。 编译器和运行时环境都不容许在寄存器中缓存该变量的值,他们必须确保当有对volatile
变量的写时,值直接被写到主内存中去以便于其余线程能够立马看到这个改变。一样的道理,读volatile
变量时,也会清除缓存而后从主内存中从新读取数据。这也致使在volatile变量时会禁止
reorder
volatile
在JSR133中,任然是不容许被重排序的,这也使得重排序它周围的正常变量变得更难,不过这不是多线程程序编写者关心的问题=_=. 对一个
volatile变量的写操做就相似于释放一个监视器(
monitor),对一个
volatile`变量的读就相似于申请得到一个监视器('monitor')。
好比说:
classVolatileExample{
int x =0;
volatileboolean v =false;
publicvoid writer(){
x =42;
v =true;
}
publicvoid reader(){
if(v ==true){
//x能够保证是42
}
}
}
考虑一个线程调用write方法一个线程调用reader方法,write方法写42到主内存中并释放监视器, read方法申请监视器并从主内存中读取42.
从上面的介绍能够看出来volatile
的做用很相似于synchronization
,均可以保证获取到最新值, 因此对于volatile
变量的读写都具备原子性,可是必须注意的是x++这种复合操做或者多个volatile
操做是不具备原子性的,也就是结果不定的。