17.1.同步
Java编程语言为线程之间的通信提供了多种机制。这些方法中最基本的是同步,它是通过以下方式实现的监测器。Java中的每个对象都与一个监视器相关联,一个线程可以锁或解锁。一次只有一个线程可以在监视器上持有锁。任何试图锁定该监视器的其他线程都会被阻塞,直到它们能够获得该监视器上的锁为止。一根线t可以多次锁定特定监视器;每次解锁都会逆转一个锁定操作的效果。
这,这个,那,那个synchronized声明(第14.19节)计算对象的引用;然后尝试在该对象的监视器上执行锁定操作,并且在锁定操作成功完成之前不会继续执行。在执行锁定操作后,synchronized语句被执行。如果身体的执行已经完成,无论是正常的还是突然的,都会在同一监视器上自动执行解锁操作。
A synchronized方法(§8.4.3.6)在被调用时自动执行锁操作;在锁操作成功完成之前不会执行它的主体。如果该方法是一个实例方法,它将锁定与其被调用的实例相关联的监视器(即将被称为this在执行该方法的主体时)。如果方法是static,它锁定与Class对象,该对象表示定义方法的类。如果方法的主体的执行一直在正常或突然完成,则会在同一监视器上自动执行解锁操作。
Java编程语言既不阻止也不需要检测死锁条件。线程在多个对象上持有(直接或间接)锁的程序应该使用传统的技术来避免死锁,如果必要的话,应该创建更高级别的锁定原语,而不是死锁。
其他机制,如读写volatile变量和类在java.util.concurrent包,提供其他同步方式。
17.2.等待集和通知
每个对象,除了有一个关联的监视器外,都有一个关联的等待集。等待集是一组线程。
当第一次创建对象时,它的等待集为空。将线程添加到等待集并从等待集中删除线程的基本操作是原子操作。等待集仅通过以下方法进行操作Object.wait, Object.notify,和Object.notifyAll.
等待集操作也可能受线程的中断状态以及Thread处理中断的类方法。此外,Thread类用于休眠和连接其他线程的方法具有来自等待和通知操作的属性。
17.2.1.等,等候
等待动作在调用时发生wait(),或者时间形式wait(long millisecs)和wait(long millisecs, int nanosecs).
号召.wait(long millisecs)的参数为零,或调用wait(long millisecs, int nanosecs)具有两个零参数的情况下,等效于调用wait().
一根线正常返回如果它返回时没有抛出InterruptedException.
让螺纹t是执行wait对象方法m,然后让n是锁操作的数目t在……上面m还没有与解锁操作相匹配。发生下列行动之一:
如果n为零(即线程t还没有拥有目标的锁。m),然后IllegalMonitorStateException被扔了。
如果这是一个定时等待,并且nanosecs参数不在0-999999或者millisecs参数是否定的,那么IllegalArgumentException被扔了。
中频螺纹t被打断,然后InterruptedException被抛出t中断状态设置为false。
否则,会出现以下顺序:
螺纹t添加到对象的等待集中。m,并执行n解锁动作m.
螺纹t不执行任何进一步的指令,直到从m的等待集。由于以下任何操作之一,线程可能会从等待集中删除,并将在以后的某个时间继续执行:
A notify正在执行的行动m其中t从等待集中移除。
A notifyAll正在执行的行动m.
阿interrupt正在执行的行动t.
如果这是时间等待,则内部操作将删除。t从…m的等待集,至少发生在millisecs毫秒加nanosecs从这个等待动作开始后,纳秒就过去了。
执行的内部行动。虽然不鼓励实现,但允许执行“虚假的唤醒”,即从等待集中删除线程,从而在没有明确指令的情况下启用恢复。
请注意,这一条款要求Java编码实践使用wait只有在循环中,只有当线程等待的某个逻辑条件保持时,循环才会终止。
每个线程必须确定可能导致从等待集中删除的事件的顺序。该顺序不一定与其他命令一致,但线程的行为必须像这些事件按该顺序发生一样。
例如,如果线程t在等待设置为m,然后两个中断t的通知m发生时,必须有关于这些事件的顺序。如果中断被认为是首先发生的,那么t最终会从wait抛掷InterruptedException,以及“等待”设置中的其他线程。m(如果在发出通知时存在任何情况)必须收到通知。如果通知被认为是首先发生的,那么t将最终正常返回wait中断还在等待中。
螺纹t执行n锁定动作m.
中频螺纹t移除m由于中断而在步骤2中设置等待,然后t的中断状态设置为false,并且wait方法抛InterruptedException.
17.2.2.通知
在调用方法时发生通知操作notify和notifyAll.
让螺纹t是在对象上执行这些方法之一的线程。m,然后让n是锁操作的数目t在……上面m还没有与解锁操作相匹配。发生下列行动之一:
如果n为零,那么IllegalMonitorStateException被扔了。
在这种情况下,线程t还没有拥有目标的锁。m.
如果n大于零,这是notify行动,那么如果m的等待集不是空的,一个线程u那是.的成员之一。m当前的等待集被选中并从等待集中删除。
没有关于在等待集中选择哪个线程的保证。从等待集中删除将启用u在等待行动中恢复。但是,请注意u恢复时的锁定操作要到一段时间后才能成功t完全解锁监视器m.
如果n大于零,这是notifyAll操作,则所有线程都将从m等待设置,然后继续。
但是,请注意,每次只有其中一个将锁定恢复等待期间所需的监视器。
17.2.3.中断
调用时发生中断操作。Thread.interrupt,以及为依次调用它而定义的方法,例如ThreadGroup.interrupt.
放任t是线程调用u.interrupt,为了某个线程u,在哪里t和u可能是一样的。此行为导致u将中断状态设置为true。
另外,如果存在某个对象m其等待集包含u,然后u从m的等待集。这使u若要在等待操作中恢复,在这种情况下,此等待将在重新锁定后恢复。m监视器,扔InterruptedException.
调用Thread.isInterrupted可以确定线程的中断状态。这,这个,那,那个static方法Thread.interrupted线程可能会调用它来观察和清除自己的中断状态。
17.2.4.等待、通知和中断的交互作用
上述规范允许我们确定与等待、通知和中断的交互有关的几个属性。
如果一个线程在等待时被通知和中断,它可以:
正常返回wait,同时仍有一个挂起的中断(换句话说,调用Thread.interrupted将返回真)
从wait抛出InterruptedException
线程可能不会重置其中断状态,并且可能从调用wait.
同样,不能因为中断而丢失通知。假设一个集合s的线程在对象的等待集中。m,而另一个线程执行notify在……上面m。然后要么:
至少有一个线程s必须正常从wait,或
所有的线程s必须退出wait抛掷InterruptedException.
注意,如果线程同时被中断和唤醒notify,该线程将从wait抛出InterruptedException,则必须通知等待集中的其他线程。
17.3.睡眠与产量
Thread.sleep使当前执行的线程在指定的持续时间内休眠(暂时停止执行),这取决于系统定时器和调度器的精度和准确性。线程不会失去对任何监视器的所有权,执行的恢复将取决于调度和执行线程所依赖的处理器的可用性。
重要的是要注意Thread.sleep也不Thread.yield有任何同步语义。尤其是,编译器不必将缓存在寄存器中的写操作刷新到共享内存中,然后调用Thread.sleep或Thread.yield之后,编译器也不必重新加载缓存在寄存器中的值。Thread.sleep或Thread.yield.
例如,在下面的(破碎的)代码片段中,假设this.done是一个非-volatile boolean字段:
while (!this.done)
Thread.sleep(1000);
编译器可以自由读取字段。this.done只需一次,并在循环的每次执行中重用缓存的值。这意味着即使另一个线程更改了this.done.
17.4.记忆模型
A 记忆模型描述给定程序和该程序的执行跟踪,该执行跟踪是否是该程序的合法执行。Java编程语言内存模型的工作方式是检查执行跟踪中的每个读,并根据一定的规则检查该读所观察到的写入是否有效。
内存模型描述程序的可能行为。一个实现可以自由地生成它喜欢的任何代码,只要程序的所有结果执行都能产生一个可以由内存模型预测的结果。
这为实现者提供了很大的自由,可以执行大量的代码转换,包括重新排序操作和消除不必要的同步。
例17.4-1。不正确的同步程序可能会显示出令人惊讶的行为
Java编程语言的语义允许编译器和微处理器执行优化,这些优化可以可能产生看似自相矛盾的行为的方式与不正确的同步代码交互。下面是一些例子,说明不正确的同步程序可能会表现出令人惊讶的行为。
考虑一下,例如,示例程序跟踪显示在表17.4-A。这个程序使用局部变量。r1和r2和共享变量A和B。一开始,A == B == 0.
表17.4-A。语句重新排序引起的令人惊讶的结果-原始代码
螺纹1 螺纹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之前,在指令4之前。从表面上看,这是荒谬的。
但是,在不影响单独执行该线程的情况下,编译器可以重新排序任何一个线程中的指令。如果指令1与指令2重新排序,如表17.4-B,则很容易看出结果如何。r2 == 2和r1 == 1可能会发生。
表17.4-B。语句重排序导致的令人惊讶的结果-有效的编译器转换
螺纹1 螺纹2
B = 1; r1 = B;
r2 = A; A = 2;
对一些程序员来说,这种行为似乎是“坏的”。但是,应该指出,此代码不正确地同步:
在一个线程中有一个写,
通过另一个线程读取同一变量,
并且,写入和读取不是按同步顺序进行的。
这种情况就是一个例子。数据竞赛 (§17.4.5)。当代码包含数据竞赛时,通常可能出现违反直觉的结果。
有几种机制可以在表17.4-B。Java虚拟机实现中的实时编译器可能会重新排列代码或处理器.此外,运行Java虚拟机实现的体系结构的内存层次结构可能会使其看起来像是正在重新排序代码。在本章中,我们将引用任何可以重新排序代码的内容。编译器.
另一个令人惊讶的结果可以从表17.4-C。一开始,p == q和p.x == 0。此程序也不正确地同步;它写入共享内存而不强制执行这些写入之间的任何顺序。
表17.4-C。前向替换引起的令人惊讶的结果
螺纹1 螺纹2
r1 = p; r6 = p;
r2 = r1.x; r6.x = 3;
r3 = q;
r4 = r3.x;
r5 = r1.x;
一个常见的编译器优化包括读取r2再用r5*他们都读到r1.x没有介入的写作。这种情况显示在表17.4-D.
表17.4-D。前向替换引起的令人惊讶的结果
螺纹1 螺纹2
r1 = p; r6 = p;
r2 = r1.x; r6.x = 3;
r3 = q;
r4 = r3.x;
r5 = r2;
现在考虑一下分配给r6.x在线程2中,第一次读取r1.x以及读到r3.x在线程1中。如果编译器决定重用r2为r5,然后r2和r5会有价值0,和r4会有价值3。从程序员的角度来看,存储在p.x变了0到3然后又变回来了。
内存模型决定程序中每个点可以读取哪些值。每个独立线程的行为必须遵循该线程的语义,但每次读取所看到的值是由内存模型决定的例外。当我们提到这一点时,我们说程序是服从的。线程内语义。线程内语义是单线程程序的语义,它允许根据线程中的读操作所看到的值来完全预测线程的行为。以确定线程的操作是否t在执行是合法的,我们简单地评估线程的实现。t它将在单线程上下文中执行,如本规范的其余部分所定义的那样。
每次线程的评估t生成线程间操作,则必须与线程间操作匹配。a的t然后按程序顺序排列。如果a是读的,然后进一步评估t使用a由内存模型决定。
本节提供了Java编程语言内存模型的规范,除了处理final字段,在§17.5.
这里指定的内存模型从根本上说并不是基于Java编程语言的面向对象特性。为了简洁和简单,我们经常在没有类或方法定义或显式取消引用的情况下展示代码片段。大多数示例由两个或多个线程组成,这些线程包含访问局部变量、共享全局变量或对象实例字段的语句。我们通常使用变量名,例如r1或r2若要指示方法或线程的局部变量,请执行以下操作。其他线程无法访问这些变量。
17.4.1.共享变量
可以在线程之间共享的内存称为共享内存或堆存储器.
所有实例字段,static字段和数组元素存储在堆内存中。在本章中,我们使用了这个术语变量若要同时引用字段和数组元素,请执行以下操作。
局部变量(第14.4节),形式方法参数(§8.4.1)和异常处理程序参数(第14.20节)不会在线程之间共享,并且不受内存模型的影响。
对同一个变量的两个访问(读或写)被认为是冲突如果至少有一个访问是写的。
17.4.2.行为
阿线间作用由一个线程执行的可被另一个线程检测或直接影响的操作。程序可以执行几种线程间操作:
朗读,阅读(正常的或非易挥发的)读取变量。
写(正常的或非易挥发的)写变量。
同步动作,即:
易失读。变量的不稳定的读数。
易失性写入。变量的易失性写入。
锁。锁定监视器
解锁。解锁监视器。
线程的(合成的)第一和最后的动作。
启动线程或检测线程已终止的操作(§17.4.4).
外部行动。外部操作是可以在执行之外观察到的操作,并且具有基于执行外部环境的结果。
线程发散作用 (§17.4.9)。线程发散操作仅由无限循环中的线程执行,在无限循环中不执行内存、同步或外部操作。如果一个线程执行一个线程发散操作,那么它后面将有无数个线程发散操作。
引入线程发散操作来建模线程如何导致所有其他线程停止运行并无法取得进展。
本规范只涉及线程间操作.我们不需要关注线程内的操作(例如,添加两个局部变量并将结果存储在第三个局部变量中)。如前所述,所有线程都需要遵守Java程序正确的线程内部语义。我们通常会更简洁地引用线程间的操作,就像简单地提到线程间的操作一样。行为.
行动a是由元组<描述的。t, k, v, u>,包括:
t-执行操作的线程
k-行动的类型
v-行动中涉及的变量或监测器。
对于锁定动作,v是否锁定监视器;对于解锁操作,v监视器被解锁了。
如果操作是(易失性或非易失性)读取,v正在读取的变量。
如果操作是(易失性或非易失性)写入,v正在写入的变量。
u-行动的任意唯一标识符
外部操作元组包含一个附加组件,该组件包含执行操作的线程所感知的外部操作的结果。这可能是关于该操作的成功或失败的信息,以及该操作读取的任何值。
外部操作的参数(例如,哪些字节被写入哪个套接字)不是外部动作元组的一部分。这些参数由线程中的其他操作设置,可以通过检查线程内部语义来确定。它们在内存模型中没有显式讨论。
在不终止执行的情况下,并不是所有的外部行为都是可以观察到的。非终止处决和可观察的行为将在§17.4.9.
17.4.3.程序和程序顺序
在每个线程执行的所有线程间操作中。t,程序顺序的t是一个总顺序,它反映了根据线程内语义执行这些操作的顺序。t.
一组动作是顺序一致如果所有操作都以与程序顺序一致的总顺序(执行顺序)进行,而且每个操作都读取。r变量的v看到写入的值。w到v使:
w先于r在执行顺序中,以及
没有其他的书写w‘这样w先于w“而且w“在此之前r在执行命令中。
顺序一致性是程序执行中的可见性和顺序性的有力保证。在顺序一致的执行中,对所有单独的操作(例如读和写)都有一个总的顺序,这与程序的顺序是一致的,而且每个单独的操作都是原子的,每个线程都可以立即看到。
如果程序没有数据竞争,那么程序的所有执行都将显示顺序一致。
顺序一致性和/或不受数据竞争的限制仍然允许从操作组中产生错误,这些操作需要被原子地感知,而不是。
如果我们使用顺序一致性作为内存模型,那么我们讨论过的许多编译器和处理器优化都是非法的。例如,在表17.4-C,只要写了.3到p.x发生时,需要随后读取该位置才能看到该值。
17.4.4.同步顺序
每次执行都有一个同步顺序。同步顺序是对执行的所有同步操作的总顺序。对于每个线程t,同步操作的同步顺序(§17.4.2)在t与程序顺序一致(§17.4.3)t.
同步操作导致同步关于行动的关系,定义如下:
显示器上的解锁动作m 同步性的所有后续锁定操作m(其中“后继”是根据同步顺序定义的)。
对可变变量的写入v (第8.3.1.4节) 同步性所有后续阅读v任何线程(其中“后继”是根据同步顺序定义的)。
启动线程的动作同步性它启动的线程中的第一个操作。
默认值的写入(零,false,或null)到每个变量同步性每个线程中的第一个动作。
尽管在分配包含变量的对象之前将默认值写入变量似乎有点奇怪,但从概念上讲,每个对象都是在程序开始时创建的,其默认值为默认值。
线程中的最终操作T1 同步性其他线程中的任何操作T2检测到T1已经终止了。
T2可以通过调用T1.isAlive()或T1.join().
中频螺纹T1中断线程T2,中断T1 同步性任何其他线程(包括T2)确定T2已被打断(被InterruptedException抛出或通过调用Thread.interrupted或Thread.isInterrupted).
a的来源同步性边称为a释放,而目的地称为获得.
17.4.5.发生-在命令之前
两个操作可以由发生-之前关系。如果一个行动发生-之前另一种,第一种是可见的,在第二种之前是有序的。
如果我们有两个行动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).
这,这个,那,那个wait课堂方法Object (第17.2.1节)具有与其关联的锁定和解锁操作;发生-之前关系由这些关联的操作定义。
应该指出的是,发生-之前两种行动之间的关系并不一定意味着它们必须在实现中按这种顺序进行。如果重新排序会产生与合法执行一致的结果,则不违法。
例如,对由线程构造的对象的每个字段的默认值的写入不需要在该线程开始之前发生,只要没有任何Read注意到这一事实。
更具体地说,如果两个操作共享一个发生-之前关系,它们不一定就一定是按照这种顺序发生在与它们不共享的任何代码中。发生-之前关系。例如,在处于数据竞争中的一个线程中写入另一个线程中的读可能会出现对这些读取的无序情况。
这,这个,那,那个发生-之前关系定义数据竞争何时发生。
一组同步边,S,是足量的传递闭包S使用程序顺序确定所有的发生-之前执行中的边缘。这套是独一无二的。
从上述定义可以看出:
显示器上的解锁发生-之前监视器上的每一个锁。
给…写一封信volatile字段(第8.3.1.4节) 发生-之前后来的每一次关于这个领域的阅读。
打电话给start()在一根线上发生-之前启动线程中的任何操作。
线程中的所有操作发生-之前任何其他线程都会成功地从join()在那条线上。
任何对象的默认初始化。发生-之前程序的任何其他操作(默认写入除外)。
当程序包含两个相互冲突的访问时(第17.4.1节)不按已发生的顺序排序的-在关系之前,据说包含一个数据竞赛.
线程间操作以外的操作的语义,例如数组长度的读取(第10.7条),执行检查的强制转换(§5.5, 第15.16节),以及对虚拟方法的调用(第15.12节),不受数据竞争的直接影响。
因此,数据争用不能导致不正确的行为,例如返回数组的错误长度。
程序是正确同步当且仅当所有顺序一致的执行都不存在数据竞争。
如果一个程序被正确同步,那么该程序的所有执行都将显示为顺序一致(§17.4.3).
对于程序员来说,这是一个非常有力的保证。程序员不需要对重新排序进行推理,就可以确定他们的代码是否包含数据竞争。因此,在确定其代码是否正确同步时,不需要对重新排序进行推理。一旦确定代码是正确同步的,程序员就不需要担心重新排序会影响他或她的代码。
程序必须正确同步,以避免在重新排序代码时可以观察到的违背直觉的行为。使用正确的同步并不能确保程序的整体行为是正确的。然而,它的使用确实允许程序员以一种简单的方式对程序的可能行为进行推理;正确同步程序的行为不依赖于可能的重排序。如果没有正确的同步,非常奇怪、混乱和违反直觉的行为是可能的。
我们说一读r变量的v允许观察写入w到v如果,在发生-之前执行跟踪的部分顺序:
r之前没有订购w(也就是说,情况并非如此HB(r,w)),以及
没有中间的写w“到v(即不写w“到v使.HB(w,w‘)和HB(w‘,r)).
非正式地,读一读r允许查看写入的结果。w如果没有发生-之前命令阻止阅读。
一套行动A是发生-在一致之前如果所有人都读r在……里面A,在哪里W®所看到的写操作吗?r,也不是HB(r,W®或者有一个写w在……里面A使.W.V = R.V和HB(W®,w)和HB(w,r).
在.发生-在一致之前操作集,每次读取都会看到它允许的写入。发生-之前点菜。
例17.4.5-1。在一致性之前发生
为了追踪表17.4.5-A,最初A == B == 0。痕迹可以观察到r2 == 0和r1 == 0现在仍然是发生-在一致之前,因为有执行命令,使得每个读都可以看到适当的写入。
表17.4.5-A.允许发生的行为-在一致性之前,但不允许顺序一致性。
螺纹1 螺纹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;
在此执行过程中,读取会看到稍后执行顺序中发生的写入。这似乎有违直觉,但却是被允许的。发生-之前一致性。允许读取以查看以后的写操作有时会产生不可接受的行为。
17.4.6.处决
处决E是由元组<描述的。P,A,po,SO,W,V,SW,HB>,包括:
P-一项计划
A-一套行动
阿宝-程序顺序,对于每个线程t所执行的所有操作的总顺序。t在……里面A
所以-同步顺序,它是对A
W-一个写着的函数,每读一次r在……里面A,给予W®,所看到的写动作r在……里面E.
V-一个值-写函数,每写一次w在……里面A,给予五(W)的价值w在……里面E.
西南-同步-与,部分顺序超过同步操作
血红蛋白-发生-在此之前,部分命令超过行动
请注意,在元素由执行的其他组件和格式良好的执行规则唯一确定之前,同步和发生(§17.4.7).
执行是发生-在一致之前如果它的一组动作是发生-在一致之前 (§17.4.5).
17.4.7.格式良好的处决
我们只考虑行刑。处决E = < P,A,po,SO,W,V,SW,HB如果下列情况属实,则>是格式良好的:
每次读取都会在执行过程中看到对同一个变量的写入。
所有对易失性变量的读写都是易失性操作。对所有的人来说r在……里面A,我们有W®在……里面A和W®.v = R.V。变量R.V是不稳定的当且仅当r是一个易失性读取,而变量W.V是不稳定的当且仅当w是不稳定的文字。
发生前的顺序是部分顺序。
发生前的顺序是由同步的传递闭包给出的边和程序顺序。它必须是一个有效的偏序:自反,传递性和反对称。
执行遵循线程内一致性。
对于每个线程t,由t在……里面A是否与该线程按程序顺序单独生成的相同,每次写入都是相同的。w书写价值五(W),考虑到每个读r看到价值V(W®。每个读取所看到的值由内存模型确定。给定的程序顺序必须反映将根据线程内语义执行操作的程序顺序。P.
执行是发生-在一致之前 (§17.4.6).
执行遵循同步顺序一致性。
对于所有易失读r在……里面A,也不是(r,W®或者有一个写w在……里面A使.W.V = R.V和SO(W®,w)和(w,r).
17.4.8.处决和因果关系要求
我们用f|d通过限制f到d。为所有人x在……里面d, f|d(x) = f(x),对所有人来说x不在d, f|d(x)是未定义的。
我们用p|d表示偏序的限制p中的元素d。为所有人x,y在……里面d, p(x,y)当且仅当p|d(x,y)。如果x或y不在d,那就不是这样了p|d(x,y).
结构良好的行刑E = < P,A,po,SO,W,V,SW,HB>由承诺来自A。如果所有的行动A可以提交,然后执行满足Java编程语言内存模型的因果关系要求。
从空集开始,作为C0,我们执行一系列步骤,在这些步骤中,我们从一组操作中采取行动。A并将它们添加到一组已提交的操作中。Ci以获得一组新的已提交操作CI+1。以证明这是合理的,对每个Ci我们需要演示执行E含Ci满足某些条件。
正式的,处决E满足Java编程语言内存模型的因果关系要求当且仅当存在:
成套行动C0, C1.。使:
C0是空集
Ci是CI+1
A = ∪ (C0, C1, …)
如果A是有限的,那么序列C0, C1.。将是有限的,结束于一组Cn = A.
如果A是无限的,那么序列C0, C1.。可能是无限的,并且必须是这个无穷序列的所有元素之和等于A.
格式良好的处决E1.,在哪里Ei = < P,Ai坡i,所以iWi、Vi西南iHbi >.
鉴于这些行动C0.。和处决E1.。,每一个动作Ci必须是Ei。所有行动Ci必须共享相同的相对发生-在顺序和同步顺序之间Ei和E。正式:
Ci的子集Ai
血红蛋白i|Ci = 血红蛋白|Ci
所以i|Ci = 所以|Ci
中写入的值。Ci两者必须是相同的Ei和E。只有读入C一-一需要看到相同的写入Ei如E。正式:
Vi|Ci = V|Ci
Wi|C一-一 = W|C一-一
全读Ei不在里面C一-一必须看到发生的事情-在他们之前。每读r在……里面Ci - C一-一必须看到写入C一-一两种Ei和E,但可能会看到另一种书写方式。Ei从它所看到的E。正式:
任何阅读r在……里面Ai - C一-一,我们有血红蛋白i(W)i®,r)
任何阅读r在(Ci - C一-一),我们有Wi®在……里面C一-一和W®在……里面C一-一
给出了一组足够的同步边Ei,如果在(§17.4.5)您正在提交的操作,那么这对必须出现在所有的操作中。Ej,在哪里j ≥ i。正式:
放任亚细亚i成为西南i的传递约简中的边。血红蛋白i但不是在阿宝。我们打电话亚细亚i这,这个,那,那个足够的同步-与边Ei。如果亚细亚i(x,y)和血红蛋白i(y,z)和z在……里面Ci,然后西南j(x,y)为所有人j ≥ i.
如果行动y已提交,所有发生的外部操作-之前-y也承诺。
如果y在Ci, x是一种外部行动血红蛋白i(x,y),然后x在……里面Ci.
实例17.4.8-1。在一致性还不够之前就发生了
在一致性之前发生,这是一组必要的约束,但不是足够的约束。仅仅是强制执行-在一致性允许不可接受的行为之前-那些违反我们为程序建立的要求的行为。例如,在一致性允许值出现“稀薄”之前,就会发生这种情况。这可以通过对表17.4.8-A.
表17.4.8-A.在一致性还不够之前就发生了
螺纹1 螺纹2
r1 = x; r2 = y;
if (r1 != 0) y = 1; if (r2 != 0) x = 1;
中所示的代码表17.4.8-A是正确同步的。这似乎令人惊讶,因为它不执行任何同步操作。但是,请记住,如果程序以顺序一致的方式执行时,不存在数据竞争,则程序是正确同步的。如果以顺序一致的方式执行此代码,则每个操作都将按程序顺序执行,并且两种写入都不会发生。由于不发生写操作,所以不可能有数据竞赛:程序是正确同步的。
由于这个程序是正确同步的,我们唯一可以允许的行为是顺序一致的行为。然而,这个程序的执行是在一致之前发生的,但不是顺序一致的:
r1 = x; // sees write of x = 1
y = 1;
r2 = y; // sees write of y = 1
x = 1;
这一结果是在一致之前发生的-没有发生-在防止其发生的关系之前。但是,这显然是不可接受的:没有顺序一致的执行会导致这种行为。事实上,我们允许读取看到稍后执行顺序中的写,这有时会导致不可接受的行为。
虽然允许读取查看稍后执行顺序中的写入有时是不可取的,但有时也是必要的。正如我们在上面看到的,表17.4.5-A需要一些读取才能看到稍后在执行顺序中发生的写入。因为读取在每个线程中都是第一位的,所以执行顺序中的第一个操作必须是读。如果该读取不能看到稍后发生的写入,则它将看不到它所读取的变量的初始值以外的任何值。这显然不是所有行为的反映。
我们提到的问题是,什么时候读可以看到将来的写因果关系,因为在案件中出现的问题,如表17.4.8-A。在这种情况下,读导致写发生,写导致读发生。这些行为没有“第一原因”。因此,我们的内存模型需要一种一致的方法来确定哪些读取可以早期看到写。
例如,在表17.4.8-A在说明读是否可以看到稍后执行过程中发生的写入时,请说明规范必须小心(请记住,如果读看到在执行过程中稍后发生的写入,则说明写入实际上是提前执行的)。
内存模型以给定的执行和程序作为输入,并确定该执行是否为程序的合法执行。它通过逐步构建一组“已提交”的操作来实现这一点,这些操作反映了程序执行了哪些操作。通常,下一个要提交的操作将反映通过顺序一致的执行可以执行的下一个操作。然而,为了反映需要看后面写的读,我们允许某些操作比发生在它们之前的其他操作更早地提交。
显然,有些行动可能会提前实施,而有些行动可能不会实施。例如,如果其中一个写在表17.4.8-A在读取该变量之前提交,读取可以看到写入,“稀薄”结果可能会发生。非正式地,如果我们知道可以在不发生某些数据竞争的情况下执行操作,那么我们允许尽早提交操作。在……里面表17.4.8-A,我们不能尽早执行这两种写入操作,因为除非读取看到数据竞争的结果,否则不会发生写入。
17.4.9.可观察行为与非终止处决
对于总是在一定有限制的有限时间内终止的程序,它们的行为可以(非正式地)简单地根据它们允许的执行来理解。对于不能在有限的时间内终止的程序,会出现更微妙的问题。
程序的可观测行为是由程序可能执行的有限组外部动作定义的。例如,一个简单地永远打印“Hello”的程序由一组用于任何非负整数的行为描述。i,包括打印“Hello”的行为。i时代。
终止没有显式建模为行为,但程序可以很容易地扩展以生成额外的外部操作。执行当所有线程终止时都会发生这种情况。
我们还定义了一个特殊的悬行动。如果行为由一组外部操作(包括悬操作,它表示在观察到外部操作之后,程序可以运行无限长的时间而不执行任何额外的外部操作或终止的行为。如果所有线程都被阻塞,或者程序可以在不执行任何外部操作的情况下执行无限数量的操作,则程序可以挂起。
线程可以在多种情况下被阻塞,例如当它试图获取依赖于外部数据的锁或执行外部操作(例如读取)时。
执行可能导致线程无限期地被阻塞,并且执行不会终止。在这种情况下,由阻塞线程生成的操作必须包括该线程直到并包括导致线程被阻塞的操作,以及该线程在该操作之后将生成的任何操作。
要对可观察的行为进行推理,我们需要讨论一组可观察的行为。
如果O是用于执行的一组可观察的操作。E,然后设置O必须是E他的行为,A,并且必须只包含有限数量的操作,即使A包含无限数量的操作。此外,如果一项行动y在O,或者HB(x,y)或(x,y),然后x在O.
注意,一组可观察的操作不限于外部操作。相反,只有在一组可观察的行动中的外部行动才被认为是可观察的外部行动。
行为B是程序的允许行为。P当且仅当B是一组有限的外部行为,并且:
有一个执行E的P,还有一套O可观察的行动E,和B中的外部操作集。O(如果有任何线程在E以阻塞状态结束O中包含的所有操作E,然后B也可能包含悬行动);或
有一个集合O所采取的行动B由一个悬中的所有外部操作O对所有人来说k ≥ | O有一个行刑E的P用行动A,并且存在一组操作。O‘这样:
双管齐下O和O的子集A满足了一组可观察的行动的要求。
O ⊆ O’ ⊆ A
| O’ | ≥ k
O’ - O不包含任何外部操作。
注意一个行为B中的外部操作的顺序。B可以观察到,但是关于如何生成和执行外部操作的其他(内部)约束可能会施加这样的约束。
17.5. final字段语义
声明为Final的字段只初始化一次,但在正常情况下不会更改。详细语义学final字段与正常字段略有不同。特别是,编译器有很大的自由移动阅读。final跨同步障碍的字段和对任意或未知方法的调用。相应地,编译器可以保留final字段缓存在寄存器中,在非-final字段必须重新加载。
final字段还允许程序员在不同步的情况下实现线程安全的不可变对象。线程安全的不可变对象被所有线程视为不可变,即使数据争用于在线程之间传递对不可变对象的引用。这可以提供安全保证,防止不正确或恶意代码滥用不可变的类。final字段必须正确使用,以保证不变性。
对象被认为是完全初始化当构造函数完成时。只有在对象被完全初始化后才能看到对象的引用的线程,保证看到该对象的初始化值是正确的final田野。
使用模式final字段是一个简单的字段:设置final对象的构造函数中的对象的字段;并且不要在对象的构造函数完成之前,在另一个线程可以看到对象的地方写入对正在构造的对象的引用。如果遵循这一点,那么当对象被另一个线程看到时,该线程将始终看到该对象的正确构造版本final田野。它还将看到由这些对象或数组引用的任何对象或数组的版本。final字段至少与final田里是。
实例17.5-1。finalJava内存模型中的字段
下面的程序说明了如何final字段与正常字段相比。
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; // guaranteed to see 3
int j = f.y; // could see 0
}
}
}
全班FinalFieldExample有一个final int场域x一个非-final int场域y。一个线程可能执行该方法。writer而另一个则可能执行该方法。reader.
因为writer方法写入f 后对象的构造函数结束,reader方法的初始化值。f.x*它将读取值。3。然而,f.y不是final;reader因此,不能保证会看到值。4为了它。
实例17.5-2。final安全字段
final字段的设计是为了提供必要的安全保证。考虑以下程序。执行一个线程(我们称之为线程1):
Global.s = “/tmp/usr”.substring(4);
当另一个线程(线程2)执行时
String myS = Global.s;
if (myS.equals("/tmp"))System.out.println(myS);
String对象是不可变的,字符串操作不执行同步。而String实现没有任何数据竞争,其他代码可能有涉及使用String对象,并且内存模型为具有数据竞争的程序提供了弱的保证。尤其是,如果String阶级不是final,那么线程2就有可能(虽然不太可能)看到默认值0对于String对象的偏移量,允许将其比较为等于“/tmp“.对String对象可能会看到4,所以String对象被认为是/usr“.Java编程语言的许多安全特性依赖于String对象被认为是真正不可变的,即使恶意代码正在使用数据竞赛传递。String线程之间的引用。
17.5.1.语义学final田
放任o成为一个对象,并且c构造器o其中一个final场域f已经写好了。一个冰冻对.采取行动final场域f的o发生在c通常或突然退出。
注意,如果一个构造函数调用另一个构造函数,而被调用的构造函数设置一个final字段,冻结final字段发生在被调用构造函数的末尾。
对于每一次执行,读的行为都会受到两个附加的偏序,即取消引用链的影响。取消()和记忆链MC(),它们被认为是执行的一部分(因此,对于任何特定的执行都是固定的)。这些部分订单必须满足以下约束(这些约束不需要唯一的解决方案):
解除引用链:如果一个动作a是对象的字段或元素的读或写。o用线t没有初始化o,那么一定会有一些阅读。r按线t的地址o使.r 取消(r,a).
内存链:内存链排序有几个约束:
如果r是看到写入的读。w,那么一定是这样的MC(w,r).
如果r和a这样的行为取消(r,a),那么一定是这样的MC(r,a).
如果w是对象地址的写入。o用线t没有初始化o,那么一定会有一些阅读。r按线t的地址o使.MC(r,w).
写出来w冻结f,一种行动a(这不是读到final(字段),读r1.的.final冻结场f,读一读r2使.HB(w,f), HB(f,a), MC(a,r)1),和取消®1,r2),则在确定哪些值可由r2,我们认为HB(w,r)2)。(这个发生-之前排序不与其他传递关闭。发生-之前(订购)
注意,退避秩序是自反的,而且r1可以和r2.
用于阅读final字段,这是唯一被认为是在阅读final字段是通过final字段语义
17.5.2.读final施工过程中的字段
读一读final构造该对象的线程中的对象的字段按照通常的构造函数内该字段的初始化排序。发生-之前规则。如果读取发生在构造函数中设置字段之后,则会看到final字段被赋值,否则它会看到默认值。
17.5.3.后续修改final田
在某些情况下,例如反序列化,系统将需要更改final构造后对象的字段。final字段可以通过反射和其他依赖于实现的方法进行更改。具有合理语义的唯一模式是构造对象的模式,然后是final更新对象的字段。对象不应对其他线程可见,final字段,直到对final对象的字段是完整的。结冰final字段都出现在构造函数的末尾,其中final字段被设置,并且在每次修改final通过反射或其他特殊机制的场。
即使如此,也有许多复杂的问题。如果final字段初始化为常量表达式(第15.28节)在字段声明中,更改final字段不能被观察到,因为它的用途final字段在编译时被替换为常量表达式的值。
另一个问题是该规范允许对final田野。在线程中,允许重新排序final字段中对final在构造函数中不发生的字段。
实例17.5.3-1。侵略性优化final田
class A {
final int x;
A() {
x = 1;
}
int f() {
return d(this,this);
}
int d(A a1, A a2) {
int i = a1.x;
g(a1);
int j = a2.x;
return j - i;
}
static void g(A a) {
// uses reflection to change a.x to 2
}
}
在d方法时,编译器可以重新排序x以及呼吁g自由。因此,new A().f()可能会回来-1, 0,或1.
实现可以提供在final-实地安全背景。如果对象是在final-字段安全上下文,读取final该对象的字段将不会在修改后重新排序。final中出现的字段。final-现场安全环境。
A final-外地安全环境有额外的保护。如果线程看到了错误发布的对象引用,该引用允许线程查看final字段,然后,在final-字段安全上下文,读取正确发布的对象引用,它将保证看到正确的值final场。在形式主义中,在final-字段安全上下文被视为一个单独的线程(为了final(仅限于字段语义)。
在实现中,编译器不应将访问移动到final字段进入或退出final-字段安全上下文(尽管它可以在执行这样的上下文时移动,只要对象不是在该上下文中构造的)。
使用final-字段安全上下文将适合在执行器或线程池中。通过执行每个Runnable分开final-字段安全上下文,执行器可以保证不正确的访问Runnable对一个物体o不会移除final外地对其他人的担保RunnableS由同一个执行者处理。
17.5.4.写保护字段
通常,一个字段是final和static可能不会被修改。然而,System.in, System.out,和System.err是static final由于遗留原因,必须允许方法更改的字段。System.setIn, System.setOut,和System.setErr。我们将这些字段称为写保护把他们和普通的人区分开来final田野。
编译器需要将这些字段与其他字段区别对待。final田野。例如,阅读一个普通的final字段对同步是“免疫的”:锁或易失性读取所涉及的屏障不必影响从final场。由于写入保护字段的值可能会发生变化,同步事件应该会对它们产生影响。因此,语义要求将这些字段视为用户代码不能更改的正常字段,除非该用户代码位于System班级,等级。
17.6.文字撕裂
Java虚拟机实现的一个考虑因素是,每个字段和数组元素都被认为是不同的;对一个字段或元素的更新不能与任何其他字段或元素的读取或更新交互。特别是,两个单独更新字节数组相邻元素的线程不能干扰或交互,并且不需要同步以确保顺序一致性。
有些处理器不提供写入单个字节的能力。在这样的处理器上实现字节数组更新是违法的,只需读取整个单词,更新适当的字节,然后将整个单词写回内存。这个问题有时被称为文字撕裂,在不能轻松地单独更新单个字节的处理器上,还需要一些其他方法。
实例17.6-1。词语撕裂的检测
以下程序是检测单词撕裂的测试用例:
public class WordTearing extends Thread {
static final int LENGTH = 8;
static final int ITERS = 1000000;
static byte[] counts = new byte[LENGTH];
static Thread[] threads = new Thread[LENGTH];
final int id;
WordTearing(int i) {
id = i;
}
public void run() {
byte v = 0;
for (int i = 0; i < ITERS; i++) {
byte v2 = counts[id];
if (v != v2) {
System.err.println("Word-Tearing found: " +
"counts[" + id + "] = "+ v2 +
", should be " + v);
return;
}
v++;
counts[id] = v;
}
}
public static void main(String[] args) {
for (int i = 0; i < LENGTH; ++i)
(threads[i] = new WordTearing(i)).start();
}
}
17.1.同步
Java编程语言为线程之间的通信提供了多种机制。这些方法中最基本的是同步,它是通过以下方式实现的监测器。Java中的每个对象都与一个监视器相关联,一个线程可以锁或解锁。一次只有一个线程可以在监视器上持有锁。任何试图锁定该监视器的其他线程都会被阻塞,直到它们能够获得该监视器上的锁为止。一根线t可以多次锁定特定监视器;每次解锁都会逆转一个锁定操作的效果。
这,这个,那,那个synchronized声明(第14.19节)计算对象的引用;然后尝试在该对象的监视器上执行锁定操作,并且在锁定操作成功完成之前不会继续执行。在执行锁定操作后,synchronized语句被执行。如果身体的执行已经完成,无论是正常的还是突然的,都会在同一监视器上自动执行解锁操作。
A synchronized方法(§8.4.3.6)在被调用时自动执行锁操作;在锁操作成功完成之前不会执行它的主体。如果该方法是一个实例方法,它将锁定与其被调用的实例相关联的监视器(即将被称为this在执行该方法的主体时)。如果方法是static,它锁定与Class对象,该对象表示定义方法的类。如果方法的主体的执行一直在正常或突然完成,则会在同一监视器上自动执行解锁操作。
Java编程语言既不阻止也不需要检测死锁条件。线程在多个对象上持有(直接或间接)锁的程序应该使用传统的技术来避免死锁,如果必要的话,应该创建更高级别的锁定原语,而不是死锁。
其他机制,如读写volatile变量和类在java.util.concurrent包,提供其他同步方式。
17.2.等待集和通知
每个对象,除了有一个关联的监视器外,都有一个关联的等待集。等待集是一组线程。
当第一次创建对象时,它的等待集为空。将线程添加到等待集并从等待集中删除线程的基本操作是原子操作。等待集仅通过以下方法进行操作Object.wait, Object.notify,和Object.notifyAll.
等待集操作也可能受线程的中断状态以及Thread处理中断的类方法。此外,Thread类用于休眠和连接其他线程的方法具有来自等待和通知操作的属性。
17.2.1.等,等候
等待动作在调用时发生wait(),或者时间形式wait(long millisecs)和wait(long millisecs, int nanosecs).
号召.wait(long millisecs)的参数为零,或调用wait(long millisecs, int nanosecs)具有两个零参数的情况下,等效于调用wait().
一根线正常返回如果它返回时没有抛出InterruptedException.
让螺纹t是执行wait对象方法m,然后让n是锁操作的数目t在……上面m还没有与解锁操作相匹配。发生下列行动之一:
如果n为零(即线程t还没有拥有目标的锁。m),然后IllegalMonitorStateException被扔了。
如果这是一个定时等待,并且nanosecs参数不在0-999999或者millisecs参数是否定的,那么IllegalArgumentException被扔了。
中频螺纹t被打断,然后InterruptedException被抛出t中断状态设置为false。
否则,会出现以下顺序:
螺纹t添加到对象的等待集中。m,并执行n解锁动作m.
螺纹t不执行任何进一步的指令,直到从m的等待集。由于以下任何操作之一,线程可能会从等待集中删除,并将在以后的某个时间继续执行:
A notify正在执行的行动m其中t从等待集中移除。
A notifyAll正在执行的行动m.
阿interrupt正在执行的行动t.
如果这是时间等待,则内部操作将删除。t从…m的等待集,至少发生在millisecs毫秒加nanosecs从这个等待动作开始后,纳秒就过去了。
执行的内部行动。虽然不鼓励实现,但允许执行“虚假的唤醒”,即从等待集中删除线程,从而在没有明确指令的情况下启用恢复。
请注意,这一条款要求Java编码实践使用wait只有在循环中,只有当线程等待的某个逻辑条件保持时,循环才会终止。
每个线程必须确定可能导致从等待集中删除的事件的顺序。该顺序不一定与其他命令一致,但线程的行为必须像这些事件按该顺序发生一样。
例如,如果线程t在等待设置为m,然后两个中断t的通知m发生时,必须有关于这些事件的顺序。如果中断被认为是首先发生的,那么t最终会从wait抛掷InterruptedException,以及“等待”设置中的其他线程。m(如果在发出通知时存在任何情况)必须收到通知。如果通知被认为是首先发生的,那么t将最终正常返回wait中断还在等待中。
螺纹t执行n锁定动作m.
中频螺纹t移除m由于中断而在步骤2中设置等待,然后t的中断状态设置为false,并且wait方法抛InterruptedException.
17.2.2.通知
在调用方法时发生通知操作notify和notifyAll.
让螺纹t是在对象上执行这些方法之一的线程。m,然后让n是锁操作的数目t在……上面m还没有与解锁操作相匹配。发生下列行动之一:
如果n为零,那么IllegalMonitorStateException被扔了。
在这种情况下,线程t还没有拥有目标的锁。m.
如果n大于零,这是notify行动,那么如果m的等待集不是空的,一个线程u那是.的成员之一。m当前的等待集被选中并从等待集中删除。
没有关于在等待集中选择哪个线程的保证。从等待集中删除将启用u在等待行动中恢复。但是,请注意u恢复时的锁定操作要到一段时间后才能成功t完全解锁监视器m.
如果n大于零,这是notifyAll操作,则所有线程都将从m等待设置,然后继续。
但是,请注意,每次只有其中一个将锁定恢复等待期间所需的监视器。
17.2.3.中断
调用时发生中断操作。Thread.interrupt,以及为依次调用它而定义的方法,例如ThreadGroup.interrupt.
放任t是线程调用u.interrupt,为了某个线程u,在哪里t和u可能是一样的。此行为导致u将中断状态设置为true。
另外,如果存在某个对象m其等待集包含u,然后u从m的等待集。这使u若要在等待操作中恢复,在这种情况下,此等待将在重新锁定后恢复。m监视器,扔InterruptedException.
调用Thread.isInterrupted可以确定线程的中断状态。这,这个,那,那个static方法Thread.interrupted线程可能会调用它来观察和清除自己的中断状态。
17.2.4.等待、通知和中断的交互作用
上述规范允许我们确定与等待、通知和中断的交互有关的几个属性。
如果一个线程在等待时被通知和中断,它可以:
正常返回wait,同时仍有一个挂起的中断(换句话说,调用Thread.interrupted将返回真)
从wait抛出InterruptedException
线程可能不会重置其中断状态,并且可能从调用wait.
同样,不能因为中断而丢失通知。假设一个集合s的线程在对象的等待集中。m,而另一个线程执行notify在……上面m。然后要么:
至少有一个线程s必须正常从wait,或
所有的线程s必须退出wait抛掷InterruptedException.
注意,如果线程同时被中断和唤醒notify,该线程将从wait抛出InterruptedException,则必须通知等待集中的其他线程。
17.3.睡眠与产量
Thread.sleep使当前执行的线程在指定的持续时间内休眠(暂时停止执行),这取决于系统定时器和调度器的精度和准确性。线程不会失去对任何监视器的所有权,执行的恢复将取决于调度和执行线程所依赖的处理器的可用性。
重要的是要注意Thread.sleep也不Thread.yield有任何同步语义。尤其是,编译器不必将缓存在寄存器中的写操作刷新到共享内存中,然后调用Thread.sleep或Thread.yield之后,编译器也不必重新加载缓存在寄存器中的值。Thread.sleep或Thread.yield.
例如,在下面的(破碎的)代码片段中,假设this.done是一个非-volatile boolean字段:
while (!this.done)
Thread.sleep(1000);
编译器可以自由读取字段。this.done只需一次,并在循环的每次执行中重用缓存的值。这意味着即使另一个线程更改了this.done.
17.4.记忆模型
A 记忆模型描述给定程序和该程序的执行跟踪,该执行跟踪是否是该程序的合法执行。Java编程语言内存模型的工作方式是检查执行跟踪中的每个读,并根据一定的规则检查该读所观察到的写入是否有效。
内存模型描述程序的可能行为。一个实现可以自由地生成它喜欢的任何代码,只要程序的所有结果执行都能产生一个可以由内存模型预测的结果。
这为实现者提供了很大的自由,可以执行大量的代码转换,包括重新排序操作和消除不必要的同步。
例17.4-1。不正确的同步程序可能会显示出令人惊讶的行为
Java编程语言的语义允许编译器和微处理器执行优化,这些优化可以可能产生看似自相矛盾的行为的方式与不正确的同步代码交互。下面是一些例子,说明不正确的同步程序可能会表现出令人惊讶的行为。
考虑一下,例如,示例程序跟踪显示在表17.4-A。这个程序使用局部变量。r1和r2和共享变量A和B。一开始,A == B == 0.
表17.4-A。语句重新排序引起的令人惊讶的结果-原始代码
螺纹1 螺纹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之前,在指令4之前。从表面上看,这是荒谬的。
但是,在不影响单独执行该线程的情况下,编译器可以重新排序任何一个线程中的指令。如果指令1与指令2重新排序,如表17.4-B,则很容易看出结果如何。r2 == 2和r1 == 1可能会发生。
表17.4-B。语句重排序导致的令人惊讶的结果-有效的编译器转换
螺纹1 螺纹2
B = 1; r1 = B;
r2 = A; A = 2;
对一些程序员来说,这种行为似乎是“坏的”。但是,应该指出,此代码不正确地同步:
在一个线程中有一个写,
通过另一个线程读取同一变量,
并且,写入和读取不是按同步顺序进行的。
这种情况就是一个例子。数据竞赛 (§17.4.5)。当代码包含数据竞赛时,通常可能出现违反直觉的结果。
有几种机制可以在表17.4-B。Java虚拟机实现中的实时编译器可能会重新排列代码或处理器.此外,运行Java虚拟机实现的体系结构的内存层次结构可能会使其看起来像是正在重新排序代码。在本章中,我们将引用任何可以重新排序代码的内容。编译器.
另一个令人惊讶的结果可以从表17.4-C。一开始,p == q和p.x == 0。此程序也不正确地同步;它写入共享内存而不强制执行这些写入之间的任何顺序。
表17.4-C。前向替换引起的令人惊讶的结果
螺纹1 螺纹2
r1 = p; r6 = p;
r2 = r1.x; r6.x = 3;
r3 = q;
r4 = r3.x;
r5 = r1.x;
一个常见的编译器优化包括读取r2再用r5*他们都读到r1.x没有介入的写作。这种情况显示在表17.4-D.
表17.4-D。前向替换引起的令人惊讶的结果
螺纹1 螺纹2
r1 = p; r6 = p;
r2 = r1.x; r6.x = 3;
r3 = q;
r4 = r3.x;
r5 = r2;
现在考虑一下分配给r6.x在线程2中,第一次读取r1.x以及读到r3.x在线程1中。如果编译器决定重用r2为r5,然后r2和r5会有价值0,和r4会有价值3。从程序员的角度来看,存储在p.x变了0到3然后又变回来了。
内存模型决定程序中每个点可以读取哪些值。每个独立线程的行为必须遵循该线程的语义,但每次读取所看到的值是由内存模型决定的例外。当我们提到这一点时,我们说程序是服从的。线程内语义。线程内语义是单线程程序的语义,它允许根据线程中的读操作所看到的值来完全预测线程的行为。以确定线程的操作是否t在执行是合法的,我们简单地评估线程的实现。t它将在单线程上下文中执行,如本规范的其余部分所定义的那样。
每次线程的评估t生成线程间操作,则必须与线程间操作匹配。a的t然后按程序顺序排列。如果a是读的,然后进一步评估t使用a由内存模型决定。
本节提供了Java编程语言内存模型的规范,除了处理final字段,在§17.5.
这里指定的内存模型从根本上说并不是基于Java编程语言的面向对象特性。为了简洁和简单,我们经常在没有类或方法定义或显式取消引用的情况下展示代码片段。大多数示例由两个或多个线程组成,这些线程包含访问局部变量、共享全局变量或对象实例字段的语句。我们通常使用变量名,例如r1或r2若要指示方法或线程的局部变量,请执行以下操作。其他线程无法访问这些变量。
17.4.1.共享变量
可以在线程之间共享的内存称为共享内存或堆存储器.
所有实例字段,static字段和数组元素存储在堆内存中。在本章中,我们使用了这个术语变量若要同时引用字段和数组元素,请执行以下操作。
局部变量(第14.4节),形式方法参数(§8.4.1)和异常处理程序参数(第14.20节)不会在线程之间共享,并且不受内存模型的影响。
对同一个变量的两个访问(读或写)被认为是冲突如果至少有一个访问是写的。
17.4.2.行为
阿线间作用由一个线程执行的可被另一个线程检测或直接影响的操作。程序可以执行几种线程间操作:
朗读,阅读(正常的或非易挥发的)读取变量。
写(正常的或非易挥发的)写变量。
同步动作,即:
易失读。变量的不稳定的读数。
易失性写入。变量的易失性写入。
锁。锁定监视器
解锁。解锁监视器。
线程的(合成的)第一和最后的动作。
启动线程或检测线程已终止的操作(§17.4.4).
外部行动。外部操作是可以在执行之外观察到的操作,并且具有基于执行外部环境的结果。
线程发散作用 (§17.4.9)。线程发散操作仅由无限循环中的线程执行,在无限循环中不执行内存、同步或外部操作。如果一个线程执行一个线程发散操作,那么它后面将有无数个线程发散操作。
引入线程发散操作来建模线程如何导致所有其他线程停止运行并无法取得进展。
本规范只涉及线程间操作.我们不需要关注线程内的操作(例如,添加两个局部变量并将结果存储在第三个局部变量中)。如前所述,所有线程都需要遵守Java程序正确的线程内部语义。我们通常会更简洁地引用线程间的操作,就像简单地提到线程间的操作一样。行为.
行动a是由元组<描述的。t, k, v, u>,包括:
t-执行操作的线程
k-行动的类型
v-行动中涉及的变量或监测器。
对于锁定动作,v是否锁定监视器;对于解锁操作,v监视器被解锁了。
如果操作是(易失性或非易失性)读取,v正在读取的变量。
如果操作是(易失性或非易失性)写入,v正在写入的变量。
u-行动的任意唯一标识符
外部操作元组包含一个附加组件,该组件包含执行操作的线程所感知的外部操作的结果。这可能是关于该操作的成功或失败的信息,以及该操作读取的任何值。
外部操作的参数(例如,哪些字节被写入哪个套接字)不是外部动作元组的一部分。这些参数由线程中的其他操作设置,可以通过检查线程内部语义来确定。它们在内存模型中没有显式讨论。
在不终止执行的情况下,并不是所有的外部行为都是可以观察到的。非终止处决和可观察的行为将在§17.4.9.
17.4.3.程序和程序顺序
在每个线程执行的所有线程间操作中。t,程序顺序的t是一个总顺序,它反映了根据线程内语义执行这些操作的顺序。t.
一组动作是顺序一致如果所有操作都以与程序顺序一致的总顺序(执行顺序)进行,而且每个操作都读取。r变量的v看到写入的值。w到v使:
w先于r在执行顺序中,以及
没有其他的书写w‘这样w先于w“而且w“在此之前r在执行命令中。
顺序一致性是程序执行中的可见性和顺序性的有力保证。在顺序一致的执行中,对所有单独的操作(例如读和写)都有一个总的顺序,这与程序的顺序是一致的,而且每个单独的操作都是原子的,每个线程都可以立即看到。
如果程序没有数据竞争,那么程序的所有执行都将显示顺序一致。
顺序一致性和/或不受数据竞争的限制仍然允许从操作组中产生错误,这些操作需要被原子地感知,而不是。
如果我们使用顺序一致性作为内存模型,那么我们讨论过的许多编译器和处理器优化都是非法的。例如,在表17.4-C,只要写了.3到p.x发生时,需要随后读取该位置才能看到该值。
17.4.4.同步顺序
每次执行都有一个同步顺序。同步顺序是对执行的所有同步操作的总顺序。对于每个线程t,同步操作的同步顺序(§17.4.2)在t与程序顺序一致(§17.4.3)t.
同步操作导致同步关于行动的关系,定义如下:
显示器上的解锁动作m 同步性的所有后续锁定操作m(其中“后继”是根据同步顺序定义的)。
对可变变量的写入v (第8.3.1.4节) 同步性所有后续阅读v任何线程(其中“后继”是根据同步顺序定义的)。
启动线程的动作同步性它启动的线程中的第一个操作。
默认值的写入(零,false,或null)到每个变量同步性每个线程中的第一个动作。
尽管在分配包含变量的对象之前将默认值写入变量似乎有点奇怪,但从概念上讲,每个对象都是在程序开始时创建的,其默认值为默认值。
线程中的最终操作T1 同步性其他线程中的任何操作T2检测到T1已经终止了。
T2可以通过调用T1.isAlive()或T1.join().
中频螺纹T1中断线程T2,中断T1 同步性任何其他线程(包括T2)确定T2已被打断(被InterruptedException抛出或通过调用Thread.interrupted或Thread.isInterrupted).
a的来源同步性边称为a释放,而目的地称为获得.
17.4.5.发生-在命令之前
两个操作可以由发生-之前关系。如果一个行动发生-之前另一种,第一种是可见的,在第二种之前是有序的。
如果我们有两个行动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).
这,这个,那,那个wait课堂方法Object (第17.2.1节)具有与其关联的锁定和解锁操作;发生-之前关系由这些关联的操作定义。
应该指出的是,发生-之前两种行动之间的关系并不一定意味着它们必须在实现中按这种顺序进行。如果重新排序会产生与合法执行一致的结果,则不违法。
例如,对由线程构造的对象的每个字段的默认值的写入不需要在该线程开始之前发生,只要没有任何Read注意到这一事实。
更具体地说,如果两个操作共享一个发生-之前关系,它们不一定就一定是按照这种顺序发生在与它们不共享的任何代码中。发生-之前关系。例如,在处于数据竞争中的一个线程中写入另一个线程中的读可能会出现对这些读取的无序情况。
这,这个,那,那个发生-之前关系定义数据竞争何时发生。
一组同步边,S,是足量的传递闭包S使用程序顺序确定所有的发生-之前执行中的边缘。这套是独一无二的。
从上述定义可以看出:
显示器上的解锁发生-之前监视器上的每一个锁。
给…写一封信volatile字段(第8.3.1.4节) 发生-之前后来的每一次关于这个领域的阅读。
打电话给start()在一根线上发生-之前启动线程中的任何操作。
线程中的所有操作发生-之前任何其他线程都会成功地从join()在那条线上。
任何对象的默认初始化。发生-之前程序的任何其他操作(默认写入除外)。
当程序包含两个相互冲突的访问时(第17.4.1节)不按已发生的顺序排序的-在关系之前,据说包含一个数据竞赛.
线程间操作以外的操作的语义,例如数组长度的读取(第10.7条),执行检查的强制转换(§5.5, 第15.16节),以及对虚拟方法的调用(第15.12节),不受数据竞争的直接影响。
因此,数据争用不能导致不正确的行为,例如返回数组的错误长度。
程序是正确同步当且仅当所有顺序一致的执行都不存在数据竞争。
如果一个程序被正确同步,那么该程序的所有执行都将显示为顺序一致(§17.4.3).
对于程序员来说,这是一个非常有力的保证。程序员不需要对重新排序进行推理,就可以确定他们的代码是否包含数据竞争。因此,在确定其代码是否正确同步时,不需要对重新排序进行推理。一旦确定代码是正确同步的,程序员就不需要担心重新排序会影响他或她的代码。
程序必须正确同步,以避免在重新排序代码时可以观察到的违背直觉的行为。使用正确的同步并不能确保程序的整体行为是正确的。然而,它的使用确实允许程序员以一种简单的方式对程序的可能行为进行推理;正确同步程序的行为不依赖于可能的重排序。如果没有正确的同步,非常奇怪、混乱和违反直觉的行为是可能的。
我们说一读r变量的v允许观察写入w到v如果,在发生-之前执行跟踪的部分顺序:
r之前没有订购w(也就是说,情况并非如此HB(r,w)),以及
没有中间的写w“到v(即不写w“到v使.HB(w,w‘)和HB(w‘,r)).
非正式地,读一读r允许查看写入的结果。w如果没有发生-之前命令阻止阅读。
一套行动A是发生-在一致之前如果所有人都读r在……里面A,在哪里W®所看到的写操作吗?r,也不是HB(r,W®或者有一个写w在……里面A使.W.V = R.V和HB(W®,w)和HB(w,r).
在.发生-在一致之前操作集,每次读取都会看到它允许的写入。发生-之前点菜。
例17.4.5-1。在一致性之前发生
为了追踪表17.4.5-A,最初A == B == 0。痕迹可以观察到r2 == 0和r1 == 0现在仍然是发生-在一致之前,因为有执行命令,使得每个读都可以看到适当的写入。
表17.4.5-A.允许发生的行为-在一致性之前,但不允许顺序一致性。
螺纹1 螺纹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;
在此执行过程中,读取会看到稍后执行顺序中发生的写入。这似乎有违直觉,但却是被允许的。发生-之前一致性。允许读取以查看以后的写操作有时会产生不可接受的行为。
17.4.6.处决
处决E是由元组<描述的。P,A,po,SO,W,V,SW,HB>,包括:
P-一项计划
A-一套行动
阿宝-程序顺序,对于每个线程t所执行的所有操作的总顺序。t在……里面A
所以-同步顺序,它是对A
W-一个写着的函数,每读一次r在……里面A,给予W®,所看到的写动作r在……里面E.
V-一个值-写函数,每写一次w在……里面A,给予五(W)的价值w在……里面E.
西南-同步-与,部分顺序超过同步操作
血红蛋白-发生-在此之前,部分命令超过行动
请注意,在元素由执行的其他组件和格式良好的执行规则唯一确定之前,同步和发生(§17.4.7).
执行是发生-在一致之前如果它的一组动作是发生-在一致之前 (§17.4.5).
17.4.7.格式良好的处决
我们只考虑行刑。处决E = < P,A,po,SO,W,V,SW,HB如果下列情况属实,则>是格式良好的:
每次读取都会在执行过程中看到对同一个变量的写入。
所有对易失性变量的读写都是易失性操作。对所有的人来说r在……里面A,我们有W®在……里面A和W®.v = R.V。变量R.V是不稳定的当且仅当r是一个易失性读取,而变量W.V是不稳定的当且仅当w是不稳定的文字。
发生前的顺序是部分顺序。
发生前的顺序是由同步的传递闭包给出的边和程序顺序。它必须是一个有效的偏序:自反,传递性和反对称。
执行遵循线程内一致性。
对于每个线程t,由t在……里面A是否与该线程按程序顺序单独生成的相同,每次写入都是相同的。w书写价值五(W),考虑到每个读r看到价值V(W®。每个读取所看到的值由内存模型确定。给定的程序顺序必须反映将根据线程内语义执行操作的程序顺序。P.
执行是发生-在一致之前 (§17.4.6).
执行遵循同步顺序一致性。
对于所有易失读r在……里面A,也不是(r,W®或者有一个写w在……里面A使.W.V = R.V和SO(W®,w)和(w,r).
17.4.8.处决和因果关系要求
我们用f|d通过限制f到d。为所有人x在……里面d, f|d(x) = f(x),对所有人来说x不在d, f|d(x)是未定义的。
我们用p|d表示偏序的限制p中的元素d。为所有人x,y在……里面d, p(x,y)当且仅当p|d(x,y)。如果x或y不在d,那就不是这样了p|d(x,y).
结构良好的行刑E = < P,A,po,SO,W,V,SW,HB>由承诺来自A。如果所有的行动A可以提交,然后执行满足Java编程语言内存模型的因果关系要求。
从空集开始,作为C0,我们执行一系列步骤,在这些步骤中,我们从一组操作中采取行动。A并将它们添加到一组已提交的操作中。Ci以获得一组新的已提交操作CI+1。以证明这是合理的,对每个Ci我们需要演示执行E含Ci满足某些条件。
正式的,处决E满足Java编程语言内存模型的因果关系要求当且仅当存在:
成套行动C0, C1.。使:
C0是空集
Ci是CI+1
A = ∪ (C0, C1, …)
如果A是有限的,那么序列C0, C1.。将是有限的,结束于一组Cn = A.
如果A是无限的,那么序列C0, C1.。可能是无限的,并且必须是这个无穷序列的所有元素之和等于A.
格式良好的处决E1.,在哪里Ei = < P,Ai坡i,所以iWi、Vi西南iHbi >.
鉴于这些行动C0.。和处决E1.。,每一个动作Ci必须是Ei。所有行动Ci必须共享相同的相对发生-在顺序和同步顺序之间Ei和E。正式:
Ci的子集Ai
血红蛋白i|Ci = 血红蛋白|Ci
所以i|Ci = 所以|Ci
中写入的值。Ci两者必须是相同的Ei和E。只有读入C一-一需要看到相同的写入Ei如E。正式:
Vi|Ci = V|Ci
Wi|C一-一 = W|C一-一
全读Ei不在里面C一-一必须看到发生的事情-在他们之前。每读r在……里面Ci - C一-一必须看到写入C一-一两种Ei和E,但可能会看到另一种书写方式。Ei从它所看到的E。正式:
任何阅读r在……里面Ai - C一-一,我们有血红蛋白i(W)i®,r)
任何阅读r在(Ci - C一-一),我们有Wi®在……里面C一-一和W®在……里面C一-一
给出了一组足够的同步边Ei,如果在(§17.4.5)您正在提交的操作,那么这对必须出现在所有的操作中。Ej,在哪里j ≥ i。正式:
放任亚细亚i成为西南i的传递约简中的边。血红蛋白i但不是在阿宝。我们打电话亚细亚i这,这个,那,那个足够的同步-与边Ei。如果亚细亚i(x,y)和血红蛋白i(y,z)和z在……里面Ci,然后西南j(x,y)为所有人j ≥ i.
如果行动y已提交,所有发生的外部操作-之前-y也承诺。
如果y在Ci, x是一种外部行动血红蛋白i(x,y),然后x在……里面Ci.
实例17.4.8-1。在一致性还不够之前就发生了
在一致性之前发生,这是一组必要的约束,但不是足够的约束。仅仅是强制执行-在一致性允许不可接受的行为之前-那些违反我们为程序建立的要求的行为。例如,在一致性允许值出现“稀薄”之前,就会发生这种情况。这可以通过对表17.4.8-A.
表17.4.8-A.在一致性还不够之前就发生了
螺纹1 螺纹2
r1 = x; r2 = y;
if (r1 != 0) y = 1; if (r2 != 0) x = 1;
中所示的代码表17.4.8-A是正确同步的。这似乎令人惊讶,因为它不执行任何同步操作。但是,请记住,如果程序以顺序一致的方式执行时,不存在数据竞争,则程序是正确同步的。如果以顺序一致的方式执行此代码,则每个操作都将按程序顺序执行,并且两种写入都不会发生。由于不发生写操作,所以不可能有数据竞赛:程序是正确同步的。
由于这个程序是正确同步的,我们唯一可以允许的行为是顺序一致的行为。然而,这个程序的执行是在一致之前发生的,但不是顺序一致的:
r1 = x; // sees write of x = 1
y = 1;
r2 = y; // sees write of y = 1
x = 1;
这一结果是在一致之前发生的-没有发生-在防止其发生的关系之前。但是,这显然是不可接受的:没有顺序一致的执行会导致这种行为。事实上,我们允许读取看到稍后执行顺序中的写,这有时会导致不可接受的行为。
虽然允许读取查看稍后执行顺序中的写入有时是不可取的,但有时也是必要的。正如我们在上面看到的,表17.4.5-A需要一些读取才能看到稍后在执行顺序中发生的写入。因为读取在每个线程中都是第一位的,所以执行顺序中的第一个操作必须是读。如果该读取不能看到稍后发生的写入,则它将看不到它所读取的变量的初始值以外的任何值。这显然不是所有行为的反映。
我们提到的问题是,什么时候读可以看到将来的写因果关系,因为在案件中出现的问题,如表17.4.8-A。在这种情况下,读导致写发生,写导致读发生。这些行为没有“第一原因”。因此,我们的内存模型需要一种一致的方法来确定哪些读取可以早期看到写。
例如,在表17.4.8-A在说明读是否可以看到稍后执行过程中发生的写入时,请说明规范必须小心(请记住,如果读看到在执行过程中稍后发生的写入,则说明写入实际上是提前执行的)。
内存模型以给定的执行和程序作为输入,并确定该执行是否为程序的合法执行。它通过逐步构建一组“已提交”的操作来实现这一点,这些操作反映了程序执行了哪些操作。通常,下一个要提交的操作将反映通过顺序一致的执行可以执行的下一个操作。然而,为了反映需要看后面写的读,我们允许某些操作比发生在它们之前的其他操作更早地提交。
显然,有些行动可能会提前实施,而有些行动可能不会实施。例如,如果其中一个写在表17.4.8-A在读取该变量之前提交,读取可以看到写入,“稀薄”结果可能会发生。非正式地,如果我们知道可以在不发生某些数据竞争的情况下执行操作,那么我们允许尽早提交操作。在……里面表17.4.8-A,我们不能尽早执行这两种写入操作,因为除非读取看到数据竞争的结果,否则不会发生写入。
17.4.9.可观察行为与非终止处决
对于总是在一定有限制的有限时间内终止的程序,它们的行为可以(非正式地)简单地根据它们允许的执行来理解。对于不能在有限的时间内终止的程序,会出现更微妙的问题。
程序的可观测行为是由程序可能执行的有限组外部动作定义的。例如,一个简单地永远打印“Hello”的程序由一组用于任何非负整数的行为描述。i,包括打印“Hello”的行为。i时代。
终止没有显式建模为行为,但程序可以很容易地扩展以生成额外的外部操作。执行当所有线程终止时都会发生这种情况。
我们还定义了一个特殊的悬行动。如果行为由一组外部操作(包括悬操作,它表示在观察到外部操作之后,程序可以运行无限长的时间而不执行任何额外的外部操作或终止的行为。如果所有线程都被阻塞,或者程序可以在不执行任何外部操作的情况下执行无限数量的操作,则程序可以挂起。
线程可以在多种情况下被阻塞,例如当它试图获取依赖于外部数据的锁或执行外部操作(例如读取)时。
执行可能导致线程无限期地被阻塞,并且执行不会终止。在这种情况下,由阻塞线程生成的操作必须包括该线程直到并包括导致线程被阻塞的操作,以及该线程在该操作之后将生成的任何操作。
要对可观察的行为进行推理,我们需要讨论一组可观察的行为。
如果O是用于执行的一组可观察的操作。E,然后设置O必须是E他的行为,A,并且必须只包含有限数量的操作,即使A包含无限数量的操作。此外,如果一项行动y在O,或者HB(x,y)或(x,y),然后x在O.
注意,一组可观察的操作不限于外部操作。相反,只有在一组可观察的行动中的外部行动才被认为是可观察的外部行动。
行为B是程序的允许行为。P当且仅当B是一组有限的外部行为,并且:
有一个执行E的P,还有一套O可观察的行动E,和B中的外部操作集。O(如果有任何线程在E以阻塞状态结束O中包含的所有操作E,然后B也可能包含悬行动);或
有一个集合O所采取的行动B由一个悬中的所有外部操作O对所有人来说k ≥ | O有一个行刑E的P用行动A,并且存在一组操作。O‘这样:
双管齐下O和O的子集A满足了一组可观察的行动的要求。
O ⊆ O’ ⊆ A
| O’ | ≥ k
O’ - O不包含任何外部操作。
注意一个行为B中的外部操作的顺序。B可以观察到,但是关于如何生成和执行外部操作的其他(内部)约束可能会施加这样的约束。
17.5. final字段语义
声明为Final的字段只初始化一次,但在正常情况下不会更改。详细语义学final字段与正常字段略有不同。特别是,编译器有很大的自由移动阅读。final跨同步障碍的字段和对任意或未知方法的调用。相应地,编译器可以保留final字段缓存在寄存器中,在非-final字段必须重新加载。
final字段还允许程序员在不同步的情况下实现线程安全的不可变对象。线程安全的不可变对象被所有线程视为不可变,即使数据争用于在线程之间传递对不可变对象的引用。这可以提供安全保证,防止不正确或恶意代码滥用不可变的类。final字段必须正确使用,以保证不变性。
对象被认为是完全初始化当构造函数完成时。只有在对象被完全初始化后才能看到对象的引用的线程,保证看到该对象的初始化值是正确的final田野。
使用模式final字段是一个简单的字段:设置final对象的构造函数中的对象的字段;并且不要在对象的构造函数完成之前,在另一个线程可以看到对象的地方写入对正在构造的对象的引用。如果遵循这一点,那么当对象被另一个线程看到时,该线程将始终看到该对象的正确构造版本final田野。它还将看到由这些对象或数组引用的任何对象或数组的版本。final字段至少与final田里是。
实例17.5-1。finalJava内存模型中的字段
下面的程序说明了如何final字段与正常字段相比。
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; // guaranteed to see 3
int j = f.y; // could see 0
}
}
}
全班FinalFieldExample有一个final int场域x一个非-final int场域y。一个线程可能执行该方法。writer而另一个则可能执行该方法。reader.
因为writer方法写入f 后对象的构造函数结束,reader方法的初始化值。f.x*它将读取值。3。然而,f.y不是final;reader因此,不能保证会看到值。4为了它。
实例17.5-2。final安全字段
final字段的设计是为了提供必要的安全保证。考虑以下程序。执行一个线程(我们称之为线程1):
Global.s = “/tmp/usr”.substring(4);
当另一个线程(线程2)执行时
String myS = Global.s;
if (myS.equals("/tmp"))System.out.println(myS);
String对象是不可变的,字符串操作不执行同步。而String实现没有任何数据竞争,其他代码可能有涉及使用String对象,并且内存模型为具有数据竞争的程序提供了弱的保证。尤其是,如果String阶级不是final,那么线程2就有可能(虽然不太可能)看到默认值0对于String对象的偏移量,允许将其比较为等于“/tmp“.对String对象可能会看到4,所以String对象被认为是/usr“.Java编程语言的许多安全特性依赖于String对象被认为是真正不可变的,即使恶意代码正在使用数据竞赛传递。String线程之间的引用。
17.5.1.语义学final田
放任o成为一个对象,并且c构造器o其中一个final场域f已经写好了。一个冰冻对.采取行动final场域f的o发生在c通常或突然退出。
注意,如果一个构造函数调用另一个构造函数,而被调用的构造函数设置一个final字段,冻结final字段发生在被调用构造函数的末尾。
对于每一次执行,读的行为都会受到两个附加的偏序,即取消引用链的影响。取消()和记忆链MC(),它们被认为是执行的一部分(因此,对于任何特定的执行都是固定的)。这些部分订单必须满足以下约束(这些约束不需要唯一的解决方案):
解除引用链:如果一个动作a是对象的字段或元素的读或写。o用线t没有初始化o,那么一定会有一些阅读。r按线t的地址o使.r 取消(r,a).
内存链:内存链排序有几个约束:
如果r是看到写入的读。w,那么一定是这样的MC(w,r).
如果r和a这样的行为取消(r,a),那么一定是这样的MC(r,a).
如果w是对象地址的写入。o用线t没有初始化o,那么一定会有一些阅读。r按线t的地址o使.MC(r,w).
写出来w冻结f,一种行动a(这不是读到final(字段),读r1.的.final冻结场f,读一读r2使.HB(w,f), HB(f,a), MC(a,r)1),和取消®1,r2),则在确定哪些值可由r2,我们认为HB(w,r)2)。(这个发生-之前排序不与其他传递关闭。发生-之前(订购)
注意,退避秩序是自反的,而且r1可以和r2.
用于阅读final字段,这是唯一被认为是在阅读final字段是通过final字段语义
17.5.2.读final施工过程中的字段
读一读final构造该对象的线程中的对象的字段按照通常的构造函数内该字段的初始化排序。发生-之前规则。如果读取发生在构造函数中设置字段之后,则会看到final字段被赋值,否则它会看到默认值。
17.5.3.后续修改final田
在某些情况下,例如反序列化,系统将需要更改final构造后对象的字段。final字段可以通过反射和其他依赖于实现的方法进行更改。具有合理语义的唯一模式是构造对象的模式,然后是final更新对象的字段。对象不应对其他线程可见,final字段,直到对final对象的字段是完整的。结冰final字段都出现在构造函数的末尾,其中final字段被设置,并且在每次修改final通过反射或其他特殊机制的场。
即使如此,也有许多复杂的问题。如果final字段初始化为常量表达式(第15.28节)在字段声明中,更改final字段不能被观察到,因为它的用途final字段在编译时被替换为常量表达式的值。
另一个问题是该规范允许对final田野。在线程中,允许重新排序final字段中对final在构造函数中不发生的字段。
实例17.5.3-1。侵略性优化final田
class A {
final int x;
A() {
x = 1;
}
int f() {
return d(this,this);
}
int d(A a1, A a2) {
int i = a1.x;
g(a1);
int j = a2.x;
return j - i;
}
static void g(A a) {
// uses reflection to change a.x to 2
}
}
在d方法时,编译器可以重新排序x以及呼吁g自由。因此,new A().f()可能会回来-1, 0,或1.
实现可以提供在final-实地安全背景。如果对象是在final-字段安全上下文,读取final该对象的字段将不会在修改后重新排序。final中出现的字段。final-现场安全环境。
A final-外地安全环境有额外的保护。如果线程看到了错误发布的对象引用,该引用允许线程查看final字段,然后,在final-字段安全上下文,读取正确发布的对象引用,它将保证看到正确的值final场。在形式主义中,在final-字段安全上下文被视为一个单独的线程(为了final(仅限于字段语义)。
在实现中,编译器不应将访问移动到final字段进入或退出final-字段安全上下文(尽管它可以在执行这样的上下文时移动,只要对象不是在该上下文中构造的)。
使用final-字段安全上下文将适合在执行器或线程池中。通过执行每个Runnable分开final-字段安全上下文,执行器可以保证不正确的访问Runnable对一个物体o不会移除final外地对其他人的担保RunnableS由同一个执行者处理。
17.5.4.写保护字段
通常,一个字段是final和static可能不会被修改。然而,System.in, System.out,和System.err是static final由于遗留原因,必须允许方法更改的字段。System.setIn, System.setOut,和System.setErr。我们将这些字段称为写保护把他们和普通的人区分开来final田野。
编译器需要将这些字段与其他字段区别对待。final田野。例如,阅读一个普通的final字段对同步是“免疫的”:锁或易失性读取所涉及的屏障不必影响从final场。由于写入保护字段的值可能会发生变化,同步事件应该会对它们产生影响。因此,语义要求将这些字段视为用户代码不能更改的正常字段,除非该用户代码位于System班级,等级。
17.6.文字撕裂
Java虚拟机实现的一个考虑因素是,每个字段和数组元素都被认为是不同的;对一个字段或元素的更新不能与任何其他字段或元素的读取或更新交互。特别是,两个单独更新字节数组相邻元素的线程不能干扰或交互,并且不需要同步以确保顺序一致性。
有些处理器不提供写入单个字节的能力。在这样的处理器上实现字节数组更新是违法的,只需读取整个单词,更新适当的字节,然后将整个单词写回内存。这个问题有时被称为文字撕裂,在不能轻松地单独更新单个字节的处理器上,还需要一些其他方法。
实例17.6-1。词语撕裂的检测
以下程序是检测单词撕裂的测试用例:
public class WordTearing extends Thread {
static final int LENGTH = 8;
static final int ITERS = 1000000;
static byte[] counts = new byte[LENGTH];
static Thread[] threads = new Thread[LENGTH];
final int id;
WordTearing(int i) {
id = i;
}
public void run() {
byte v = 0;
for (int i = 0; i < ITERS; i++) {
byte v2 = counts[id];
if (v != v2) {
System.err.println("Word-Tearing found: " +
"counts[" + id + "] = "+ v2 +
", should be " + v);
return;
}
v++;
counts[id] = v;
}
}
public static void main(String[] args) {
for (int i = 0; i < LENGTH; ++i)
(threads[i] = new WordTearing(i)).start();
}
}