合肥建行网站,北京市专业网站建设,佛山集团网站建设,手机端网页制作1.什么是内存模型#xff1f; 在多核系统中#xff0c;处理器一般有一层或者多层的缓存#xff0c;这些的缓存通过加速数据访问#xff08;因为数据距离处理器更近#xff09;和降低共享内存在总线上的通讯#xff08;因为本地缓存能够满足许多内存操作#xff09;来提高…1.什么是内存模型 在多核系统中处理器一般有一层或者多层的缓存这些的缓存通过加速数据访问因为数据距离处理器更近和降低共享内存在总线上的通讯因为本地缓存能够满足许多内存操作来提高CPU性能。缓存能够大大提升性能但是它们也带来了许多挑战。例如当两个CPU同时检查相同的内存地址时会发生什么在什么样的条件下它们会看到相同的值 在处理器层面上内存模型定义了一个充要条件“让当前的处理器可以看到其他处理器写入到内存的数据”以及“其他处理器可以看到当前处理器写入到内存的数据”。有些处理器有很强的内存模型(strong memory model)能够让所有的处理器在任何时候任何指定的内存地址上都可以看到完全相同的值。而另外一些处理器则有较弱的内存模型weaker memory model在这种处理器中必须使用内存屏障一种特殊的指令来刷新本地处理器缓存并使本地处理器缓存无效目的是为了让当前处理器能够看到其他处理器的写操作或者让其他处理器能看到当前处理器的写操作。这些内存屏障通常在lock和unlock操作的时候完成。内存屏障在高级语言中对程序员是不可见的。 在强内存模型下有时候编写程序可能会更容易因为减少了对内存屏障的依赖。但是即使在一些最强的内存模型下内存屏障仍然是必须的。设置内存屏障往往与我们的直觉并不一致。近来处理器设计的趋势更倾向于弱的内存模型因为弱内存模型削弱了缓存一致性所以在多处理器平台和更大容量的内存下可以实现更好的可伸缩性 “一个线程的写操作对其他线程可见”这个问题是因为编译器对代码进行重排序导致的。例如只要代码移动不会改变程序的语义当编译器认为程序中移动一个写操作到后面会更有效的时候编译器就会对代码进行移动。如果编译器推迟执行一个操作其他线程可能在这个操作执行完之前都不会看到该操作的结果这反映了缓存的影响。 此外写入内存的操作能够被移动到程序里更前的时候。在这种情况下其他的线程在程序中可能看到一个比它实际发生更早的写操作。所有的这些灵活性的设计是为了通过给编译器运行时或硬件灵活性使其能在最佳顺序的情况下来执行操作。在内存模型的限定之内我们能够获取到更高的性能。 Java内存模型描述了在多线程代码中哪些行为是合法的以及线程如何通过内存进行交互。它描述了“程序中的变量“ 和 ”从内存或者寄存器获取或存储它们的底层细节”之间的关系。Java内存模型通过使用各种各样的硬件和编译器的优化来正确实现以上事情。 Java包含了几个语言级别的关键字包括volatile final以及synchronized目的是为了帮助程序员向编译器描述一个程序的并发需求。Java内存模型定义了volatile和synchronized的行为更重要的是保证了同步的java程序在所有的处理器架构下面都能正确的运行。
2.其他语言像C也有内存模型吗
大部分其他的语言像C和C都没有被设计成直接支持多线程。这些语言对于发生在编译器和处理器平台架构的重排序行为的保护机制会严重的依赖于程序中所使用的线程库例如pthreads编译器以及代码所运行的平台所提供的保障。
3.Java内存模型FAQ三JSR133是什么
从1997年以来人们不断发现Java语言规范的17章定义的Java内存模型中的一些严重的缺陷。这些缺陷会导致一些使人迷惑的行为例如final字段会被观察到值的改变和破坏编译器常见的优化能力。Java内存模型是一个雄心勃勃的计划它是编程语言规范第一次尝试合并一个能够在各种处理器架构中为并发提供一致语义的内存模型。不过定义一个既一致又直观的内存模型远比想象要更难。JSR133为Java语言定义了一个新的内存模型它修复了早期内存模型中的缺陷。为了实现JSR133final和volatile的语义需要重新定义。完整的语义见: Java内存模型但是正式的语义不是小心翼翼的它是令人惊讶和清醒的目的是让人意识到一些看似简单的概念如同步其实有多复杂。幸运的是你不需要懂得这些正式语义的细节——JSR133的目的是创建一组正式语义这些正式语义提供了volatile、synchronzied和final如何工作的直观框架。
JSR 133的目标包含了
保留已经存在的安全保证像类型安全以及强化其他的安全保证。例如变量值不能凭空创建线程观察到的每个变量的值必须是被其他线程合理的设置的。正确同步的程序的语义应该尽量简单和直观。应该定义未完成或者未正确同步的程序的语义主要是为了把潜在的安全危害降到最低。程序员应该能够自信的推断多线程程序如何同内存进行交互的。能够在现在许多流行的硬件架构中设计正确以及高性能的JVM实现。应该能提供安全地初始化的保证。如果一个对象正确的构建了 意思是它的引用没有在构建的时候逸出那么所有能够看到这个对象的引用的线程在不进行同步的情况下也将能看到在构造方法中中设置的final字段的值。应该尽量不影响现有的代码。
4.重排序意味着什么
在很多情况下访问一个程序变量对象实例字段类静态字段和数组元素可能会使用不同的顺序执行而不是程序语义所指定的顺序执行。编译器能够自由的以优化的名义去改变指令顺序。在特定的环境下处理器可能会次序颠倒的执行指令。数据可能在寄存器处理器缓冲区和主内存中以不同的次序移动而不是按照程序指定的顺序。这里主要是因为CPU有缓存且CPU命令是以流水线形式执行。例如如果一个线程写入值到字段a然后写入值到字段b而且b的值不依赖于a的值那么处理器就能够自由的调整它们的执行顺序而且缓冲区能够在a之前刷新b的值到主内存。有许多潜在的重排序的来源例如编译器JIT以及缓冲区。编译器运行时和硬件被期望一起协力创建好像是顺序执行的语义的假象这意味着在单线程的程序中程序应该是不能够观察到重排序的影响的。但是重排序在没有正确同步了的多线程程序中开始起作用在这些多线程程序中一个线程能够观察到其他线程的影响也可能检测到其他线程将会以一种不同于程序语义所规定的执行顺序来访问变量。大部分情况下一个线程不会关注其他线程正在做什么但是当它需要关注的时候这时候就需要同步了。
5.旧的内存模型有什么问题 旧的内存模型中有几个严重的问题。这些问题很难理解因此被广泛的违背。例如旧的存储模型在许多情况下不允许JVM发生各种重排序行为。旧的内存模型中让人产生困惑的因素造就了JSR-133规范的诞生。 例如一个被广泛认可的概念就是如果使用final字段那么就没有必要在多个线程中使用同步来保证其他线程能够看到这个字段的值。尽管这是一个合理的假设和明显的行为也是我们所期待的结果。实际上在旧的内存模型中我们想让程序正确运行起来却是不行的。在旧的内存模型中final字段并没有同其他字段进行区别对待——这意味着同步是保证所有线程看到一个在构造方法中初始化的final字段的唯一方法。结果——如果没有正确同步的话对一个线程来说它可能看到一个字段的默认值然后在稍后的时间里又能够看到构造方法中设置的值。这意味着一些不可变的对象例如String能够改变它们值——这实在很让人郁闷。这里是因为String类型默认null而初始化是我们自定义的值也就是两种不同的值仿佛String是可变的一样 旧的内存模型允许volatile变量的写操作和非volaitle变量的读写操作一起进行重排序这和大多数的开发人员对于volatile变量的直观感受是不一致的因此会造成迷惑。 最后我们将看到的是程序员对于程序没有被正确同步的情况下将会发生什么的直观感受通常是错误的。JSR-133的目的之一就是要引起这方面的注意。
6.没有正确同步的含义是什么 没有正确同步的代码对于不同的人来说可能会有不同的理解。在Java内存模型这个语义环境下我们谈到“没有正确同步”我们的意思是 一个线程中有一个对变量的写操作另外一个线程对同一个变量有读操作而且写操作和读操作没有通过同步来保证顺序。 当这些规则被违反的时候我们就说在这个变量上有一个“数据竞争”(data race)。一个有数据竞争的程序就是一个没有正确同步的程序。
7.同步会干些什么呢 同步有几个方面的作用。最广为人知的就是互斥 ——一次只有一个线程能够获得一个监视器因此在一个监视器上面同步意味着一旦一个线程进入到监视器保护的同步块中其他的线程都不能进入到同一个监视器保护的块中间除非第一个线程退出了同步块。 但是同步的含义比互斥更广。同步保证了一个线程在同步块之前或者在同步块中的一个内存写入操作以可预知的方式对其他有相同监视器的线程可见。当我们退出了同步块我们就释放了这个监视器这个监视器有刷新缓冲区到主内存的效果因此该线程的写入操作能够为其他线程所见。在我们进入一个同步块之前我们需要获取监视器监视器有使本地处理器缓存失效的功能因此变量会从主存重新加载于是其它线程对共享变量的修改对当前线程来说就变得可见了。 依据缓存来讨论同步可能听起来这些观点仅仅会影响到多处理器的系统。但是重排序效果能够在单一处理器上面很容易见到。对编译器来说在获取之前或者释放之后移动你的代码是不可能的。当我们谈到在缓冲区上面进行的获取和释放操作我们使用了简述的方式来描述大量可能的影响。 新的内存模型语义在内存操作读取字段写入字段锁解锁以及其他线程的操作start 和 join中创建了一个部分排序在这些操作中一些操作被称为happen before其他操作。当一个操作在另外一个操作之前发生第一个操作保证能够排到前面并且对第二个操作可见。这些排序的规则如下: 线程中的每个操作happens before该线程中在程序顺序上后续的每个操作。解锁一个监视器的操作happens before随后对相同监视器进行锁的操作。对volatile字段的写操作happens before后续对相同volatile字段的读取操作。线程上调用start()方法happens before这个线程启动后的任何操作。一个线程中所有的操作都happens before从这个线程join()方法成功返回的任何其他线程。 注意思是其他线程等待一个线程的join()方法完成那么这个线程中的所有操作happens before其他线程中的所有操作
补充happens-before 偏序关系离散数学
偏序又分非严格偏序自反偏序与严格偏序反自反偏序
自反偏序 给定集合S“≤”是S上的二元关系若“≤”满足
自反性∀a∈S有a≤a反对称性∀ab∈Sa≤b且b≤a则ab传递性∀abc∈Sa≤b且b≤c则a≤c
反自反偏序 给定集合S“”是S上的二元关系若“”满足
反自反性∀a∈S有a≮a非对称性∀ab∈Sab ⇒ b≮a传递性∀abc∈Sab且bc则ac
注意这里的符号不是简单的表示大小与JMM中的happens-before不是表示时间的前后是一样理解偏序是关键的一步。
延申
严格偏序与有向无环图dag有直接的对应关系。一个集合上的严格偏序的关系图就是一个有向无环图。其传递闭包是它自己。
有向无环图的判断
深度优先遍历拓扑排序求关键路径的前提是无环能不能判断严格来说也可以
更加通俗具体的happens-before讲解
注:《深入理解Java虚拟机》中曾指出happens-before规则诞生之前是使用八大具体的原子操作定义发生的顺序
这意味着任何内存操作这个内存操作在退出一个同步块前对一个线程是可见的对任何线程在它进入一个被相同的监视器保护的同步块后都是可见的因为所有内存操作happens before释放监视器以及释放监视器happens before获取监视器。 其他如下模式的实现被一些人用来强迫实现一个内存屏障的不会生效:
synchronized (new Object()) {}这段代码其实不会执行任何操作你的编译器会把它完全移除掉因为编译器知道没有其他的线程会使用相同的监视器进行同步。要看到其他线程的结果你必须为一个线程建立happens before关系。
重点注意对两个线程来说为了正确建立happens before关系而在相同监视器上面进行同步是非常重要的。以下观点是错误的当线程A在对象X上面同步的时候所有东西对线程A可见线程B在对象Y上面进行同步的时候所有东西对线程B也是可见的。释放监视器和获取监视器必须匹配也就是说要在相同的监视器上面完成这两个操作否则代码就会存在“数据竞争”。
8.final字段如何改变它们的值 我们可以通过分析String类的实现具体细节来展示一个final变量是如何可以改变的。 String对象包含了三个字段一个character数组一个数组的offset和一个length。实现String类的基本原理为它不仅仅拥有character数组而且为了避免多余的对象分配和拷贝多个String和StringBuffer对象都会共享相同的character数组。因此String.substring()方法能够通过改变length和offset而共享原始的character数组来创建一个新的String。对一个String来说这些字段都是final型的字段。 String s1 /usr/tmp; String s2 s1.substring(4); 字符串s2的offset的值为4length的值为4。但是在旧的内存模型下对其他线程来说看到offset拥有默认的值0是可能的而且 稍后一点时间会看到正确的值4好像字符串的值从“/usr”变成了“/tmp”一样。
旧的Java内存模型允许这些行为部分JVM已经展现出这样的行为了。在新的Java内存模型里面这些是非法的。
9.在新的Java内存模型中final字段是如何工作的 一个对象的final字段值是在它的构造方法里面设置的。假设对象被正确的构造了一旦对象被构造在构造方法里面设置给final字段的的值在没有同步的情况下对所有其他的线程都会可见。另外引用这些final字段的对象或数组都将会看到final字段的最新值。 对一个对象来说被正确的构造是什么意思呢简单来说它意味着这个正在构造的对象的引用在构造期间没有被允许逸出。参见安全构造技术。换句话说不要让其他线程在其他地方能够看见一个构造期间的对象引用。不要指派给一个静态字段不要作为一个listener注册给其他对象等等。这些操作应该在构造方法之后完成而不是构造方法中来完成。
class FinalFieldExample {final int x;int y;static FinalFieldExample f;public FinalFieldExample() {x 3;
y 4;}static void writer() {f new FinalFieldExample();}static void reader() {if (f ! null) {int i f.x;int j f.y;}}
}
上面的类展示了final字段应该如何使用。一个正在执行reader方法的线程保证看到f.x的值为3因为它是final字段。它不保证看到f.y的值为4因为f.y不是final字段。如果FinalFieldExample的构造方法像这样:
public FinalFieldExample() { // bad!x 3;y 4;// bad construction - allowing this to escapeglobal.obj this;
}那么从global.obj中读取this的引用线程不会保证读取到的x的值为3。
能够看到字段的正确的构造值固然不错但是如果字段本身就是一个引用那么你还是希望你的代码能够看到引用所指向的这个对象或者数组的最新值。如果你的字段是final字段那么这是能够保证的。因此当一个final指针指向一个数组你不需要担心线程能够看到引用的最新值却看不到引用所指向的数组的最新值。重复一下这儿的“正确的”的意思是“对象构造方法结尾的最新的值”而不是“最新可用的值”。现在在讲了如上的这段之后如果在一个线程构造了一个不可变对象之后对象仅包含final字段你希望保证这个对象被其他线程正确的查看你仍然需要使用同步才行。例如没有其他的方式可以保证不可变对象的引用将被第二个线程看到。使用final字段的程序应该仔细的调试这需要深入而且仔细的理解并发在你的代码中是如何被管理的。如果你使用JNI来改变你的final字段这方面的行为是没有定义的。
10.volatile是干什么用的
Volatile字段是用于线程间通讯的特殊字段。每次读volatile字段都会看到其它线程写入该字段的最新值实际上程序员之所以要定义volatile字段是因为在某些情况下由于缓存和重排序所看到的陈旧的变量值是不可接受的。编译器和运行时禁止在寄存器里面分配它们。它们还必须保证在它们写好之后它们被从缓冲区刷新到主存中因此它们立即能够对其他线程可见。相同地在读取一个volatile字段之前缓冲区必须失效因为值是存在于主存中而不是本地处理器缓冲区。在重排序访问volatile变量的时候还有其他的限制。在旧的内存模型下访问volatile变量不能被重排序但是它们可能和访问非volatile变量一起被重排序。这破坏了volatile字段从一个线程到另外一个线程作为一个信号条件的手段。在新的内存模型下volatile变量仍然不能彼此重排序。和旧模型不同的时候volatile周围的普通字段的也不再能够随便的重排序了。写入一个volatile字段和释放监视器有相同的内存影响而且读取volatile字段和获取监视器也有相同的内存影响。事实上因为新的内存模型在重排序volatile字段访问上面和其他字段volatile或者非volatile访问上面有了更严格的约束。当线程A写入一个volatile字段f的时候如果线程B读取f的话 那么对线程A可见的任何东西都变得对线程B可见了。 如下例子展示了volatile字段应该如何使用:
class VolatileExample {int x 0;volatile boolean v false;public void writer() {x 42;v true;}public void reader() {if (v true) {//uses x - guaranteed to see 42.}}
}假设一个线程叫做“writer”另外一个线程叫做“reader”。对变量v的写操作会等到变量x写入到内存之后然后读线程就可以看见v的值。因此如果reader线程看到了v的值为true那么它也保证能够看到在之前发生的写入42这个操作。而这在旧的内存模型中却未必是这样的。如果v不是volatile变量那么编译器可以在writer线程中重排序写入操作那么reader线程中的读取x变量的操作可能会看到0。
实际上volatile的语义已经被加强了已经快达到同步的级别了。为了可见性的原因每次读取和写入一个volatile字段已经像一个半同步操作了重点注意对两个线程来说为了正确的设置happens-before关系访问相同的volatile变量是很重要的。以下的结论是不正确的当线程A写volatile字段f的时候线程A可见的所有东西在线程B读取volatile的字段g之后变得对线程B可见了。释放操作和获取操作必须匹配也就是在同一个volatile字段上面完成。
11.新的内存模型是否修复了双重锁检查问题
臭名昭著的双重锁检查也叫多线程单例模式是一个骗人的把戏它用来支持lazy初始化同时避免过度使用同步。 在非常早的JVM中同步非常慢开发人员非常希望删掉它。双重锁检查代码如下
// double-checked-locking - dont do this!
private static Something instance null;public Something getInstance() {if (instance null) {synchronized (this) {if (instance null)instance new Something();}}return instance;
}
这看起来好像非常聪明——在公用代码中避免了同步。这段代码只有一个问题 —— 它不能正常工作。为什么呢最明显的原因是初始化实例的写入操作和实例字段的写入操作能够被编译器或者缓冲区重排序重排序可能会导致返回部分构造的一些东西。就是我们读取到了一个没有初始化的对象。这段代码还有很多其他的错误以及为什么对这段代码的算法修正是错误的。在旧的java内存模型下没有办法修复它。更多深入的信息可参见Double-checkedlocking: Clever but broken and The “DoubleChecked Locking is broken” declaration许多人认为使用volatile关键字能够消除双重锁检查模式的问题。在1.5的JVM之前volatile并不能保证这段代码能够正常工作因环境而定。在新的内存模型下实例字段使用volatile可以解决双重锁检查的问题因为在构造线程来初始化一些东西和读取线程返回它的值之间有happens-before关系。然后对于喜欢使用双重锁检查的人来说我们真的希望没有人这样做仍然不是好消息。双重锁检查的重点是为了避免过度使用同步导致性能问题。从java1.0开始不仅同步会有昂贵的性能开销而且在新的内存模型下使用volatile的性能开销也有所上升几乎达到了和同步一样的性能开销。因此使用双重锁检查来实现单例模式仍然不是一个好的选择。修订—在大多数平台下volatile性能开销还是比较低的。 使用IODH来实现多线程模式下的单例会更易读:
// 还可以使用枚举静态内部类实现
private static class LazySomethingHolder {public static Something something new Something();
}public static Something getInstance() {return LazySomethingHolder.something;
} 这段代码是正确的因为初始化是由static字段来保证的。如果一个字段设置在static初始化中对其他访问这个类的线程来说是能正确的保证它的可见性的。
12.为什么我需要关注Java内存模型
为什么你需要关注java内存模型并发程序的bug非常难找。它们经常不会在测试中发生而是直到你的程序运行在高负荷的情况下才发生非常难于重现和跟踪。你需要花费更多的努力提前保证你的程序是正确同步的。这不容易但是它比调试一个没有正确同步的程序要容易的多。