计算机系统中,为了尽可能地避免处理器访问主内存的时间开销,处理器大多会利用缓存(cache)以提高性能。其模型如下图所示。
在这种模型下会存在一个现象,即缓存中的数据与主内存的数据并不是实时同步的,各CPU(或CPU核心)间缓存的数据也不是实时同步的。这导致在同一个时间点,各CPU所看到同一内存地址的数据的值可能是不一致的。从程序的视角来看,就是在同一个时间点,各个线程所看到的共享变量的值可能是不一致的。
有的观点会将这种现象也视为重排序的一种,命名为“内存系统重排序”。因为这种内存可见性问题造成的结果就好像是内存访问指令发生了重排序一样。
这种内存可见性问题也会导致章节一中示例代码即便在没有发生指令重排序的情况下的执行结果也还是(0, 0)。
实验:
package org.fenxisoft.concurrency; public class PossibleReordering { private int x = 0, y = 0; private int a = 0, b = 0; public void testReordering(int count) throws InterruptedException { Thread one = new Thread(new Runnable() { public void run() { a = 1; x = b; } }); Thread other = new Thread(new Runnable() { public void run() { b = 1; y = a; } }); one.start();other.start(); while(Thread.activeCount() > 1){ Thread.yield(); } System.out.println("第"+count+"次 (" + x + "," + y + ")"); if(x == 0 && y == 0){ System.exit(0); } } public static void main(String[] args) throws InterruptedException { for(int i = 0; i< Integer.MAX_VALUE; i++){ new PossibleReordering().testReordering(i); } } }
很容易想到这段代码的运行结果可能为(1,0)、(0,1)或(1,1),因为线程one可以在线程two开始之前就执行完了,也有可能反之,甚至有可能二者的指令是同时或交替执行的。
然而,这段代码的执行结果也可能是(0,0). 因为,在实际运行时,代码指令可能并不是严格按照代码语句顺序执行的。得到(0,0)结果的语句执行过程,如下图所示。值得注意的是,a=1和x=b这两个语句的赋值操作的顺序被颠倒了,或者说,发生了指令“重排序”(reordering)。(事实上,输出了这一结果,并不代表一定发生了指令重排序,内存可见性问题也会导致这样的输出,详见后文)
对重排序现象不太了解的开发者可能会对这种现象感到吃惊,但是,笔者开发环境下做的一个小实验证实了这一结果
实验代码是构造一个循环,反复执行上面的实例代码,直到出现a=0且b=0的输出为止。实验结果说明,循环执行到第11062341次时输出了(0,0).
大多数现代微处理器都会采用将指令乱序执行(out-of-order execution,简称OoOE或OOE)的方法,在条件允许的情况下,直接运行当前有能力立即执行的后续指令,避开获取下一条指令所需数据时造成的等待3。通过乱序执行的技术,处理器可以大大提高执行效率。
除了处理器,常见的Java运行时环境的JIT编译器也会做指令重排序操作4,即生成的机器指令与字节码指令顺序不一致。
相关推荐
不同的CPU架构对于内存访问的处理方式各不相同,这也意味着在设计和实现多线程或多处理器程序时需要特别注意内存访问的一致性和可见性。通过合理使用内存屏障,开发者可以有效地控制内存访问的顺序,从而避免潜在的...
这些重排序可能会导致多线程程序中出现内存可见性问题。 对于顺序一致性,它是指程序执行的顺序效果与程序源代码中的顺序一致。在多线程环境下,数据竞争可能破坏这种顺序一致性。Java内存模型中的同步机制,如...
总结来说,Java内存模型通过原子性、有序性和可见性保证了多线程环境下的数据一致性。理解并熟练运用这些概念是编写高效、线程安全的Java代码的基础。在实际开发中,应根据需求选择合适的方式来确保这三个特性,以...
然而,对于一些复合操作(如`i++`),由于它们涉及到多次内存访问,因此在默认情况下并不具备原子性。 **案例分析**: ```java private static int counter = 0; public static void main(String[] args) { for ...
JMM通过控制主内存与线程本地内存的交互,提供了内存可见性的保证,防止由于编译器优化、指令级并行和内存系统重排序导致的问题。为了实现这一目标,JMM会插入内存屏障指令来禁止特定类型的重排序,确保程序在不同的...
Java 内存模型(Java Memory Model,简称 JMM)是 Java 平台中关于线程如何访问共享变量的一套规则,它定义了线程之间的内存可见性、数据一致性以及指令重排序等关键概念,对于多线程编程和并发性能优化至关重要。...
现代CPU的计算速度远远高于内存的读写速度,CPU会采用高速缓存来抵消内存访问带来的延迟。为了减少CACHE_WAIT,CPU会采用指令级并行重排序来提供执行效率,也可以叫做CPU乱序执行。 在Java中,有两种方式可以保障...
- 描述了两个操作之间的顺序关系,是JMM保证内存可见性的基础。 - 例如,一个线程初始化一个对象,然后另一个线程访问这个对象,Happens-Before原则确保了初始化操作对其他线程可见。 4. **重排序** - 编译器和...
- **volatile**:标记一个变量,使其具有可见性和禁止指令重排序的效果。但是,volatile不能保证原子性。 - **synchronized**:用于同步方法或同步块,确保同一时间只有一个线程执行该代码块,提供了互斥和可见性。...
JMM的关键点在于其对并发环境下的重排序规则和内存可见性的规定。重排序是指编译器和处理器为了优化性能,可能会改变程序中原本的执行顺序。然而,这种优化在多线程环境下可能导致问题,因为它可能破坏了线程间的...
- **内存可见性问题**:不同线程间可能无法正确地看到变量的最新值,导致线程间的数据不一致。 - **并发执行问题**:当一个线程正在执行`set()`方法的同时,另一个线程尝试调用`check()`方法时,可能会导致不可预测...
Java内存模型的核心内容涵盖了锁、线程间的交互、内存可见性和顺序一致性等方面。在JSR-133之前的Java内存模型规范中,volatile变量的语义较弱,它们的访问可以自由排序。但在新规范中,volatile变量的语义被加强为...
内存屏障是JMM用来解决内存可见性问题的关键技术,它阻止特定的内存操作被重排序,确保数据在缓存和主内存之间的一致性。 3. JVM的指令重排序是性能优化的一种手段,允许编译器和处理器为了提高执行效率而改变代码...
- 这是判断数据是否存在竞争、是否需要同步的一个依据,规定了内存可见性的顺序。 7. **原子操作与CAS** - 原子操作(如AtomicInteger)在不使用锁的情况下保证了更新操作的原子性。 - CAS(Compare and Swap)...
CPU的内存模型通常分为强内存模型和弱内存模型,区别在于是否允许对主内存的修改立即可见以及是否允许编译器内存访问指令的重排序。大多数现代处理器采用弱内存模型。 JMM借鉴了CPU-缓存-主内存的模型,即线程对...