解释Java并发中的“程序顺序规则”
程序顺序规则指出“线程中的每个操作都发生在 – 该程序顺序中稍后出现的该线程中的每个操作之前”
我在另一个线程中读到了一个动作
- 读取和写入变量
- 锁定和解锁显示器
- 开始和加入线程
这是否意味着可以按顺序更改读取和写入,但读取和写入不能使用第2行或第3行中指定的操作更改顺序?
2.“程序顺序”是什么意思?
对示例的解释将非常有用。
其他相关问题
假设我有以下代码:
long tick = System.nanoTime(); //Line1: Note the time //Block1: some code whose time I wish to measure goes here long tock = System.nanoTime(); //Line2: Note the time
首先,它是一个单线程应用程序,以保持简单。 编译器注意到它需要检查两次时间并且还注意到与周围时间注释行没有依赖关系的代码块,因此它看到了重组代码的可能性,这可能导致Block1不被定时调用包围在实际执行期间(例如,考虑此顺序Line1-> Line2-> Block1)。 但是,作为程序员,我可以看到Line1,2和Block1之间的依赖关系。 Line1应该紧接在Block1之前,Block1需要有限的时间才能完成,并且Line2立即成功。
所以我的问题是:我是否正确测量了块?
- 如果是,那么是什么阻止了编译器重新排列顺序。
- 如果不是,(通过Enno的答案后认为是正确的)我该怎么做才能防止它。
PS:我最近在SO中提出的另一个问题偷了这个代码。
它可能有助于解释为什么这样的规则首先存在。
Java是一种过程语言。 即你告诉Java如何为你做点什么。 如果Java不按您编写的顺序执行您的指令,那么它显然不起作用。 例如,在下面的例子中,如果Java会做2 – > 1 – > 3那么炖菜就会被破坏。
1. Take lid off 2. Pour salt in 3. Cook for 3 hours
那么,为什么规则不是简单地说“Java按照你编写的顺序执行你所写的内容”? 简而言之,因为Java很聪明。 采用以下示例:
1. Take eggs out of the freezer 2. Take lid off 3. Take milk out of the freezer 4. Pour egg and milk in 5. Cook for 3 hours
如果Java和我一样,它只是按顺序执行它。 然而,Java足够聪明地理解它更有效并且最终结果是相同的,如果它做1 – > 3 – > 2 – > 4 – > 5(你不必再次走到冰箱,并且这不会改变配方)。
那么规则“线程中的每个动作发生在 – 在程序顺序后面的那个线程中的每个动作之前”都试图说,“在一个线程中,你的程序将运行就好像它是在精确执行的那样运行命令你写了。我们可能会改变场景背后的顺序,但我们确保这些都不会改变输出。
到现在为止还挺好。 为什么不跨多个线程做同样的事情? 在multithreading编程中,Java不够聪明,无法自动完成。 它将用于某些操作(例如,加入线程,启动线程,使用锁(监视器)等等)但是对于其他东西,您需要明确告诉它不要进行重新排序以改变程序输出(例如,字段上的volatile
标记) ,使用锁等)。
注意:
关于“发生在关系之前”的快速补遗。 这是一种奇特的说法,无论Java可能做什么重新排序,东西A都会发生在B之前。在我们奇怪的后来的炖菜例子中,“步骤1和3 发生 – 在第4步之前 ”将鸡蛋和牛奶倒入“”。 另外,例如,“步骤1和3不需要发生在之前的关系,因为它们不以任何方式相互依赖”
关于评论的其他问题和回应
首先,让我们确定编程世界中“时间”的含义。 在编程中,我们有“绝对时间”的概念(现在世界上的时间是什么?)和“相对时间”的概念(自x以来经过了多长时间?)。 在理想的世界中,时间就是时间,但除非我们内置了primefaces钟,否则绝对时间必须不时得到纠正。 另一方面,对于相对时间,我们不想要更正,因为我们只对事件之间的差异感兴趣。
在Java中, System.currentTime()
处理绝对时间, System.nanoTime()
处理相对时间。 这就是为什么nanoTime的Javadoc说:“这种方法只能用于测量经过的时间 ,与系统或挂钟时间的任何其他概念无关”。
实际上,currentTimeMillis和nanoTime都是本机调用,因此编译器实际上无法certificate重新排序是否会影响正确性,这意味着它不会重新排序执行。
但是让我们想象一下,我们想编写一个实际查看本机代码的编译器实现,只要它是合法的就重新排序。 当我们看到JLS时,它告诉我们的只是“你可以重新排序任何东西,只要它不能被检测到”。 现在作为编译器编写者,我们必须决定重新排序是否会违反语义。 对于相对时间(nanoTime),如果我们重新排序执行,它显然是无用的(即违反语义)。 现在,如果我们重新排序绝对时间(currentTimeMillis),它会违反语义吗? 只要我们能够将世界时间的来源(比如说系统时钟)与我们决定的任何东西(如“50ms”)*之间的差异限制,我就说不。 对于以下示例:
long tick = System.currentTimeMillis(); result = compute(); long tock = System.currentTimeMillis(); print(result + ":" + tick - tock);
如果编译器可以certificatecompute()
占用的数据小于我们允许的系统时钟的最大偏差,那么按以下方式重新排序是合法的:
long tick = System.currentTimeMillis(); long tock = System.currentTimeMillis(); result = compute(); print(result + ":" + tick - tock);
由于这样做不会违反我们定义的规范,因此不会违反语义。
您还问为什么这不包含在JLS中。 我认为答案是“保持JLS简短”。 但我对这个领域知之甚少,所以你可能想问一个单独的问题。
*:在实际实现中,这种差异取决于平台。
程序顺序规则保证在单个线程内 ,编译器引入的重新排序优化不会产生与程序以串行方式执行时发生的结果不同的结果。 如果没有同步的那些线程观察到它的状态,它不能保证线程的动作在任何其他线程中可能出现的顺序。
请注意,此规则仅说明程序的最终结果 ,而不是该程序中单个执行的顺序。 例如,如果我们有一个方法对一些局部变量进行以下更改:
x = 1; z = z + 1; y = 1;
编译器可以自由地重新排序这些操作,但它认为最适合提高性能。 想到这一点的一种方法是:如果您可以在源代码中重新排序这些操作并仍然获得相同的结果,编译器可以自由地执行相同的操作。 (事实上,它可以更进一步完全丢弃显示没有结果的操作,例如调用空方法。)
使用第二个项目符号点, 监视器锁定规则起作用:“监视器上的解锁发生 – 在主监视器锁定的每个后续锁定之前 。” ( Java Concurrency in Practice p.341)这意味着获取给定锁的线程将具有在释放该锁之前在其他线程中发生的操作的一致视图。 但请注意,此保证仅适用于两个不同的线程release
或acquire
相同的锁 。 如果线程A在释放Lock X之前做了很多事情,然后线程B获得了锁定Y,则线程B不能保证具有A的前X动作的一致视图。
如果a。这样做不会破坏线程内的程序顺序, 并且 b。)变量没有其他“先发生”线程同步语义,则可以使用start
和join
重新排序对变量的读取和写入。应用于它们,比如将它们存储在volatile
字段中。
一个简单的例子:
class ThreadStarter { Object a = null; Object b = null; Thread thread; ThreadStarter(Thread threadToStart) { this.thread = threadToStart; } public void aMethod() { a = new BeforeStartObject(); b = new BeforeStartObject(); thread.start(); a = new AfterStartObject(); b = new AfterStartObject(); a.doSomeStuff(); b.doSomeStuff(); } }
由于字段a
和b
以及方法aMethod()
没有以任何方式同步,并且启动thread
操作不会改变对字段的写入结果(或者对那些字段进行处理),编译器是可以自由地将thread.start()
重新排序到方法中的任何位置。 它对aMethod()
的顺序唯一不能做的就是在将BeforeStartObject
写入该字段之后将其中一个AfterStartObject
写入一个字段,或者将一个doSomeStuff()
调用移动到一个字段上。 AfterStartObject
写入之前的字段。 (也就是说,假设这样的重新排序会以某种方式改变doSomeStuff()
调用的结果。)
这里要记住的关键是,在没有同步的情况下,在aMethod()
启动的线程理论上可以观察在执行aMethod()
期间它们所处理的任何状态中的字段a
和b
中的a
或两个aMethod()
(包括null
)。
补充问题的答案
如果要在任何测量中实际使用它们,则无法对Block1
的代码重新排序tick
和tock
的赋值,例如通过计算它们之间的差异并将结果打印为输出。 这样的重新排序显然会破坏Java的线程内部as-if-serial语义。 它改变了通过执行指定程序顺序中的指令所获得的结果。 如果分配不用于任何测量并且对程序结果没有任何副作用,它们可能会被编译器优化为无操作而不是重新排序。
在我回答这个问题之前,
读取和写入变量
应该
易失性读取和易失性写入(相同字段)
程序顺序并不保证在关系之前发生这种情况,而是发生在之前的关系保证程序顺序
对你的问题:
这是否意味着可以按顺序更改读取和写入,但读取和写入不能使用第2行或第3行中指定的操作更改顺序?
答案实际上取决于首先发生的行动以及发生的行动。 看看JSR 133 Cookbook for Compiler Writers 。 有一个Can Reorder网格列出了可能发生的允许的编译器重新排序。
例如,可以在普通存储的上方或下方重新排序易失性存储,但不能在 易失性负载之上或之下重新排序易失性存储 。 这都是假设内部线程语义仍然存在。
“程序顺序”是什么意思?
这是来自JLS
在由每个线程t执行的所有线程间动作中,t的程序顺序是反映根据t的线程内语义将执行这些动作的顺序的总顺序。
换句话说,如果您可以改变变量的写入和加载,使其完全按照您编写的方式执行,那么它将维护程序顺序。
例如
public static Object getInstance(){ if(instance == null){ instance = new Object(); } return instance; }
可以重新订购
public static Object getInstance(){ Object temp = instance; if(instance == null){ temp = instance = new Object(); } return temp; }
它只是意味着线程可能是多路的,但线程的动作/操作/指令的内部顺序将保持不变(相对)
thread1:T1op1,T1op2,T1op3 … thread2:T2op1,T2op2,T2op3 ……
虽然线程中的操作顺序(Tn’op’M)可能不同,但线程内的操作T1op1, T1op2, T1op3
将始终按此顺序排列,因此T2op1, T2op2, T2op3
对于前:
T2op1, T1op1, T1op2, T2op2, T2op3, T1op3
Java教程http://docs.oracle.com/javase/tutorial/essential/concurrency/memconsist.html表示发生这种情况之前 – 关系只是保证一个特定语句的内存写入对另一个特定语句可见。 这是一个例子
int x; synchronized void x() { x += 1; } synchronized void y() { System.out.println(x); }
synchronized
创建一个发生在之前的关系,如果我们删除它将无法保证在线程A增量x线程B将打印1后,它可能会打印0