首页 > 代码库 > JVM的重排序
JVM的重排序
重排序一般是编译器或执行时环境为了优化程序性能而採取的对指令进行又一次排序执行的一种手段。重排序分为两类:编译期重排序和执行期重排序,分别相应编译时和执行时环境。
在并发程序中,程序猿会特别关注不同进程或线程之间的数据同步。特别是多个线程同一时候改动同一变量时,必须採取可靠的同步或其他措施保障数据被正确地改动。这里的一条重要原则是:不要如果指令运行的顺序,你无法预知不同线程之间的指令会以何种顺序运行。
可是在单线程程序中,通常我们easy如果指令是顺序运行的,否则能够想象程序会发生什么可怕的变化。理想的模型是:各种指令运行的顺序是唯一且有序的,这个顺序就是它们被编写在代码中的顺序。与处理器或其他因素无关,这样的模型被称作顺序一致性模型。也是基于冯·诺依曼体系的模型。
当然,这样的如果本身是合理的,在实践中也鲜有异常发生。但其实,没有哪个现代多处理器架构会採用这样的模型。由于它是在是太低效了。
而在编译优化和CPU流水线中,差点儿都涉及到指令重排序。
编译期重排序
编译期重排序的典型就是通过调整指令顺序。在不改变程序语义的前提下。尽可能降低寄存器的读取、存储次数。充分复用寄存器的存储值。
如果第一条指令计算一个值赋给变量A并存放在寄存器中,第二条指令与A无关但须要占用寄存器(如果它将占用A所在的那个寄存器),第三条指令使用A的值且与第二条指令无关。
那么如果依照顺序一致性模型,A在第一条指令运行过后被放入寄存器,在第二条指令运行时A不再存在。第三条指令运行时A又一次被读入寄存器。而这个过程中,A的值没有发生变化。通常编译器都会交换第二和第三条指令的位置。这样第一条指令结束时A存在于寄存器中,接下来能够直接从寄存器中读取A的值,减少了反复读取的开销。
重排序对于流水线的意义
现代CPU差点儿都採用流水线机制加快指令的处理速度。一般来说,一条指令须要若干个CPU时钟周期处理。而通过流水线并行运行。能够在同等的时钟周期内运行若干条指令。详细做法简单地说就是把指令分为不同的运行周期,比如读取、寻址、解析、运行等步骤。并放在不同的元件中处理。同一时候在运行单元EU中,功能单元被分为不同的元件。比如加法元件、乘法元件、载入元件、存储元件等,能够进一步实现不同的计算并行运行。
流水线架构决定了指令应该被并行运行,而不是在顺序化模型中所觉得的那样。
重排序有利于充分使用流水线,进而达到超标量的效果。
确保顺序性
虽然指令在运行时并不一定依照我们所编写的顺序运行,但毋庸置疑的是。在单线程环境下。指令运行的终于效果应当与其在顺序运行下的效果一致,否则这样的优化便会失去意义。
通常不管是在编译期还是执行期进行的指令重排序,都会满足上面的原则。
Java存储模型中的重排序
在Java存储模型(Java Memory Model, JMM)中,重排序是十分重要的一节。特别是在并发编程中。JMM通过happens-before法则保证顺序运行语义,假设想要让运行操作B的线程观察到运行操作A的线程的结果。那么A和B就必须满足happens-before原则,否则。JVM能够对它们进行随意排序以提高程序性能。
volatile
keyword能够保证变量的可见性。由于对volatile
的操作都在Main Memory中,而Main Memory是被全部线程所共享的。这里的代价就是牺牲了性能。无法利用寄存器或Cache。由于它们都不是全局的,无法保证可见性。可能产生脏读。
volatile
另一个作用就是局部阻止重排序的发生。对volatile变量的操作指令都不会被重排序,由于假设重排序,又可能产生可见性问题。在保证可见性方面,锁(包含显式锁、对象锁)以及对原子变量的读写都能够确保变量的可见性。可是实现方式略有不同。比如同步锁保证得到锁时从内存里又一次读入数据刷新缓存,释放锁时将数据写回内存以保数据可见。而volatile变量干脆都是读写内存。
JVM的重排序