本文主要是介绍【译文】Java内存模型JMM(线程和锁17.4.1-17.4.7),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
文章目录
- 原文地址说明
- JMM
- 不正确地进行程序同步可能带来意想不到的结果
- 共享变量
- 操作指令
- 程序和程序顺序
- 同步顺序
- 先后顺序
- 执行过程
- 良好规则的执行过程
原文地址说明
- The Java Language Specification, Java SE 8 Edition Java8语言规范。此文为此规范的第17章节:Chapter 17. Threads and Locks 中的17.4.1-17.4.7。
- 真的好多,歇着,后边再战
JMM
- 内存模型描述了一个既定程序和这个程序的执行情况,即:这个程序的执行是否合法。在Java语言的内存模型中,通过检测每一个读操作,并根据某些规则检查所能被读到数据对应的写操作是否合法有效。
- 内存模型,描述了一个程序可能有的行为。一种模型实现可以自由的产生它所喜欢的任何code,只要程序的执行结果是通过内存模型可以预测的。这便为实现者提供了非常大的自由去执行大量的代码转换,包括对指令的再排序以及删除不必要的同步。
不正确地进行程序同步可能带来意想不到的结果
- Java语言允许编译器和微处理器执行优化,这些优化与不正确的同步代码可以在某种方式下进行交互,从而产生一些看似矛盾的行为。有一些示例,可以很好的说明:不正确的同步程序是怎样产生意料之外结果的?
- 如表17.4-A所示的程序:这个程序使用本地变量r1和r2去共享变量A和B。A和B的初始值为0;
- 17.4-A:语句重排引起的异常结果 —原代码
Thread 1 | Thread 2 |
---|
1: r2 = A ; | 3: r1 = B ; |
2: B = 1 ; | 4: A =2; |
- 显然,r2 == 2 和 r1 == 1这是不可能的结果。直观的讲,指令1和3应该最先执行。如果先执行指令1,则就不能看到指令4的写操作。如果先执行指令3,它同样也不能看到指令2的写操作。如果某个执行出现这种行为,我们可以指导指令4应该再指令1前执行,指令2在指令3之前执行。从表面上看,这是不可能的。但是,在不影响线程的隔离性时,编译器被允许重排任一线程的指令。如果指令1和指令2的顺序重排,就会看到17.4-B的效果。很容易看出r22和r11是如何发生的。
- 17.4-B:语句重排引起的异常结果 - 有效的编译器转换
Thread 1 | Thread 2 |
---|
B=1 ; | 3: r1 = B ; |
r2=A ; | 4: A =2; |
- 对一些程序员来说,这种行为看起来是被破坏的。但仍然需要注意的是:代码进行了不适当的同步。有一个线程有写操作;另一个线程在同一个变量上有读操作;这两个操作没有被同步排序。这种情况就是数据竞争的一个例子(17.4.5).当执行代码发生数据竞争时,通常会带来意想不到的结果。
- 有几种机制可以产生表17.4-B中的重新排序。Java虚拟机实现的实时编译器可以重新排列代码或处理器。此外,运行中Java虚拟机的体系结构的内存层次结构可能使代码看起来像是在重新排序。在本章中,我们将提到任何可以将代码重新排序的编译器。另一个令人惊讶结果见表17.4-C。最初,pq和p.x0。此程序也不正确地同步;它写入共享内存而不在这些写入之间强制执行任何顺序。
- 17.4-C:由置换引起的异常结果
Thread 1 | Thread 2 |
---|
r1 = p; | r6 = p; |
r2 = r1.x; | r6.x = 3; |
r3 = q; | |
r4 = r3.x; | |
r5 = r1.x; | |
- 一个常见的编译器优化涉及到r5重用r2的值:它们都是r1.x的读取,没有中间的写入。这种情况如表17.4-D所示。
Thread 1 | Thread 2 |
---|
r1 = p; | r6 = p; |
r2 = r1.x; | r6.x = 3; |
r3 = q; | |
r4 = r3.x; | |
r5 = r2; | |
- 现在考虑在线程2中对r6.x的赋值发生在线程1中r1.x的第一次读取和r3.x的读取之间的情况。如果编译器决定重用r5的r2值,那么r2和r5的值将为0,r4的值为3。从程序员的角度来看,存储在p.x上的值从0变为3,然后又变回来了。
- 内存模型决定了程序在每个点上可以读取哪些值。每个独立线程的操作必须由该线程的语义控制,但每次读取所看到的值由内存模型决定。当我们提到这一点时,我们说程序遵循线程语义。线程语义是单线程程序的语义,它允许根据线程read操作所看到的值来完整地预测线程的行为。为了确定线程t在执行中的操作是否合法,我们只需评估线程t的实现,因为它将在单线程上下文中执行,如本规范其余部分所定义的那样。
- 每次线程t生成一个线程操作时,它必须与按程序顺序排列的t的线程间的操作a相匹配。如果a是read,那么对t的进一步求值将使用由内存模型确定a看到的值。本节提供了Java编程语言内存模型的规范,除了§17.5中描述的有关final字段的问题。这里指定的内存模型基本上不是基于Java编程语言的面向对象特性的。在我们的例子中,为了简洁和简单,我们经常展示没有类或方法定义或显式解引用的代码片段。大多数示例由两个或多个线程组成,这些线程包含可以访问对象的局部变量、共享全局变量或实例字段的语句。我们通常使用变量名,如r1或r2来表示方法或线程的局部变量。其他线程无法访问这些变量。
共享变量
- 可以在线程间共享的内存称为共享内存或者堆内存
- 所有的实例字段、静态字段和数组元素存在堆内存空间。在这个章节中,我们使用术语:变量,去指代字段和数组元素。
- 局部变量(§14.4)、方法形参(§8.4.1)和异常处理程序参数(§14.20)从不在线程之间共享,并且不受内存模型的影响。
- 如果至少有一个请求是写操作,那么对于同一个变量的两个操作,被称为冲突。
操作指令
- 一个线程内的操作是一个线程的执行,这个执行可以被另一个线程监测到或者直接影响到。这里展示几种线程间操作的执行:
- A:读(normal,or non-volatile):读取一个变量
- B:写(normal,or non-volatile):写一个变量
- C:同步操作,包括:1,volatile 读; 2,volatile 写;3,锁monitor;4,解锁monitor;5,线程的第一个和最后一个操作;6,启动线程或者监测线程终止的操作。
- D:外部操作,外部操作是指在执行之外可以观察到的动作,其结果基于执行外部的环境。
- E:线程的发散行为:仅发生在一个线程处于一个没有内存、同步或者外部操作的内循环中。如果一个线程被异常执行,那么紧接着将会有无限个线程异常执行。PS:有差异的线程行为被包含在内存模型中,展示一个线程怎样导致其他线程终止或者无法执行。
- 此规范仅涉及线程间操作。我们不需要关心线程内的操作(例如,添加两个局部变量并将结果存储到第三个局部变量中)。如前所述,所有线程都需要遵守Java程序的线程内语义。我们通常将线程间操作更简洁地称为简单操作。动作a由元组<t,k,v,u>描述,包括:t-执行操作的线程、k-动作类型、v-动作中涉及的变量或监视器。对于锁定操作,v是被锁定的监视器;对于解锁操作,v是正在解锁的监视器。如果操作是一个读取,则v是正在读取的变量。如果操作是写操作,则v是正在写入的变量、u-操作的任意唯一标识符
- 外部操作元组包含一个附加组件,该组件包含执行该操作的线程所感知到的外部操作的结果。这可能是关于操作成功或失败的信息,以及操作读取的任何值。
- 外部操作的参数(例如,写入哪个套接字的字节)不是外部操作元组的一部分。这些参数由线程内的其他操作设置,可以通过检查线程内语义来确定。它们不会在内存模型中明确讨论。在非终止执行中,并不是所有的外部动作都是可见的。第17.4.9节讨论了非终止执行和可观察行为。
程序和程序顺序
- 在每个线程t执行的所有线程间操作中,t的程序顺序是一个程序的总顺序。总顺序是根据t的线程内语义来反映这些操作的执行顺序。如果所有操作发生在一个总的执行顺序中,对于读操作 r,都能看见被 w 写入的v 变量,那么这一组操作是一致的。例如:w 在 r之前执行,并且没有其他的写w’操作,使得w在w’之前并且w’在r之前。顺序一致性是程序执行过程中可见性和顺序的一个非常有力的保证。在一个顺序一致的执行中,所有单独的操作(如读和写)有一个总的顺序,它与程序的顺序一致,并且每个单独的操作都是原子的,并且每个线程都可以立即看到。
- 如果一个程序中没有数据竞争,然后这个程序的所有执行将会是一致的。
顺序一致性和/或不受数据竞争的影响,仍然允许操作组产生错误,这些错误需要被完整的接收到。如果我们使用顺序一致性作为内存模型,那么我们讨论过的许多编译器和处理器优化都是非法的。例如,在表17.4-C中,一旦3写入p.x,则需要让此位置的后续读操作都能看到这个value。
同步顺序
- 每次执行都有一个同步顺序。同步顺序是执行的所有同步操作的总顺序。对于每个线程t,t中同步动作(§17.4.2)的同步顺序与t的程序顺序(§17.4.3)一致。同步动作包含动作上的后续同步,定义如下:
- A:监视器m上的解锁操作与m上的所有后续锁定操作同步(其中“后续”根据同步顺序定义)。
- B:对volatile变量v的写入(§8.3.1.4)与任何线程对v的所有后续读取同步(其中“后续”根据同步顺序定义)。
- C:启动线程的操作与它启动的线程中的第一个操作同步。
- D:对每个变量的默认值(零、假或空)的写入与每个线程中的第一个操作同步。虽然在分配包含变量的对象之前将默认值写入变量似乎有点奇怪,但从概念上讲,每个对象都是在程序开始时用其默认初始化值创建的。
- E:线程T1中的最后一个操作与另一个线程T2中检测到T1已终止的任何操作同步。T2可以通过调用T1.isAlive()或T1.join()来实现这一点。
- F;如果线程T1中断线程T2,T1中断将与任何其他线程(包括T2)确定T2已中断的任何点同步(通过抛出InterruptedException或调用线程中断或者线程中断).
先后顺序
- 两个动作可以由happers-before关系排序。如果一个动作发生在另一个动作之前,那么第一个动作对第二个动作是可见的,并且在第二个动作之前排序。如果我们有两个动作x和y,我们写hb(x,y)来表示x发生在y之前:如果x和y是同一线程的操作,并且x在程序顺序中在y之前,那么hb(x,y)。从一个对象的构造函数的结尾到终结器的开始(§12.6),有一个发生在边缘之前。如果一个动作x与随后的动作y同步,那么我们也有hb(x,y)。如果是hb(x,y)和hb(y,z),那么hb(x,z)。
- 类Object(§17.2.1)的wait方法有与它们相关联的lock和unlock动作;它们发生在这些关联动作定义关系之前。应该指出的是,两个动作之间的“先于发生”关系的存在并不一定意味着它们必须在执行中按该顺序进行。如果重新排序产生的结果与合法执行一致,则不违法。例如,在线程构造的对象的每个字段中写入默认值不必在该线程启动之前发生,只要没有读操作观察到这个事实。
- 更具体地说,如果两个操作共享一个“发生在前面”关系,那么对于不共享“先于发生”关系的任何代码来说,它们不一定必须以该顺序出现。例如,一个线程中的写入与另一个线程中的读取处于数据争用状态,而这些读操作的顺序似乎不一致。数据发生争抢时,用hb关系定义。
- 如果一组同步操作S是足够的小,就能使得程序顺序中的S界定所有执行中的先后关系。从上述定义可以看出:监视器上的解锁发生在该监视器上的每次后续锁定之前。对volatile字段(§8.3.1.4)的写入发生在对该字段的每次后续读取之前。对线程的start()调用发生在已启动线程中的任何操作之前。一个线程中的所有操作都发生在任何其他线程从该线程上的join()成功返回之前。任何对象的默认初始化发生在程序的任何其他操作(默认写入除外)之前。
- 当一个程序包含两个冲突的访问(§17.4.1),而这两个访问不是由happers-before关系排序的,则称其包含一个数据竞争。
- 除了线程间之外的操作语义,例如读取数组长度(§10.7)、执行检查的强制转换(§5.5,§15.16)和虚拟方法的调用(§15.12),不会直接受到数据争用的影响。因此,数据争用不会导致错误行为,例如返回错误的数组长度。当且仅当所有顺序一致的执行都不存在数据争用时,程序才会正确同步。如果一个程序被正确同步,那么该程序的所有执行将看起来是顺序一致的(§17.4.3)。PS:这对程序员来说是一个非常有力的保证。程序员不需要考虑重新排序来确定他们的代码是否包含数据竞争。因此,在确定代码是否正确同步时,它们不需要考虑重新排序。一旦确定代码是正确同步的,程序员就不必担心重新排序会影响他或她的代码。程序必须正确同步,以避免在重新排序代码时观察到的各种违反直觉的行为。使用正确的同步并不能确保程序的整体行为是正确的。然而,它的使用确实允许程序员以一种简单的方式对程序可能的行为进行推理;一个正确同步的程序的行为对可能的重新排序的依赖性要小得多。如果没有正确的同步,很奇怪、令人困惑和违反直觉的行为是可能的。
- 我们说,允许变量v的读r观察w到v的写入,如果执行的部分顺序有先后顺序:r不在w之前排序(即,不是hb(r,w)),并且没有中间写w’到v(即没有写入w’到v,使得hb(w,w’)和hb(w’,r))。非正式地说,如果在命令之前没有发生任何事情来阻止读,则允许读r看到write w的结果。一致性之前会出现一组操作A,如果对于A中的所有读取r,其中W(r)是r看到的写操作,则不是hb(r,W(r))或A中存在写入W,因此W.v=r.v和hb(W(r),W)和hb(W,r)。
- 在happers-before一致操作集中,每个读操作都会看到一个由happers-before排序允许看到的写操作。
- 17.4.5-1:happens-before一致性
- 对于表17.4.5-A,初始AB0。可以观察到r20和r10,并且在一致性之前仍然发生,这是因为存在允许每次读取看到适当写入的执行顺序
- 17.4.5-A:行为允许发生在hb一致性之前,但不是顺序一致性
Thread 1 | Thread 2 |
---|
B = 1; | A = 2; |
r2 = A; | r1 = B; |
- 由于没有同步,每次读取都可以看到初始值的写入或另一个线程的写入。此行为的执行顺序是:
1: B = 1;
3: A = 2;
2: r2 = A; // sees initial write of 0
4: r1 = B; // sees initial write of 0
1: r2 = A; // sees write of A = 2
3: r1 = B; // sees write of B = 1
2: B = 1;
4: A = 2;
- 在这个执行过程中,读操作会看到执行顺序后面发生的写入操作。这看起来有悖常理,但这会发生在hb一致性中,允许读后看写有时会产生不可接受的行为。
执行过程
- 执行E由元组<P,a,po,so,W,V,sw,hb>描述,包括:P-a计划、A-一组动作、po-程序顺序,对于每个线程t,是t在a中执行的所有操作的总顺序、so-同步顺序,所有A中的同步操作的总顺、W-它是一个write-seen函数,对于a中的每个读r,给出W(r),即r在E中看到的写操作、V—一个写值的函数,对于a中的每一个写w,给出V(w),即w在E中写的值、sw-一组同步操作的后续顺序、hb-先后顺序。
- 注意,同步操作的后续顺序和先后顺序的元素是由执行的其他组件和良好的执行规则唯一确定的(§17.4.7)。如果一组动作有先后顺序一致性,则此执行有先后顺序一致性。(§17.4.5)。
良好规则的执行过程
- 如果一个执行E = < P, A, po, so, W, V, sw, hb >满足以下描述,我们称它为良好规则的执行。
- A:每一次读都会看见对同一个变量的写操作。所有对volatile变量的读写都是volatile操作。对于A中的所有读操作r,我们在A中有W(r)和W(r).v=r.v。当且仅当r是volatile读取时,变量r.v是volatile的;当且仅当W是volatile写入时,变量W.v是volatile的。
- B: 先后顺序是一个偏序。先后hb顺序由同步后续的顺序和程序顺序给出。它必须是一个有效的偏序:自反、传递和反对称。
- C:遵循线程间一致性。对于每个线程t,在A中由t执行的操作与该线程单独按程序顺序生成的操作相同,每个write w写入值V(w),前提是每个读r都看到值V(w(r))。每次读取所看到的值由内存模型决定。给定的程序顺序必须反映根据P的线程内语义执行操作的程序顺序。
- D:符合hb顺序一致性(§17.4.6)。
- E:符合同步顺序一致性。对于A中的所有volatile读操作r,不是so(r,W(r))或A中存在写W的情况,即W.v=r.v,so(W(r),W)和so(W,r)。
这篇关于【译文】Java内存模型JMM(线程和锁17.4.1-17.4.7)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!