Java内存模型简析

时间:2024-01-02 13:31:39

1、多线程基础

  线程通信,是指线程之间以何种机制来交换信息。其中通信的机制有两种:内存共享和消息传递。内存共享是指线程之间通过写-读内存中的公共状态隐式进行通讯(Java);消息传递在线程之间没有公共状态,线程之间必须明确通过发送消息来显式进行通讯(Erlang)。

  同步是指程序用于控制不同线程之间操作发生的相对顺序的机制。在内存共享机制中,同步是显式的,而在消息传递机制中同步是隐式的(消息的发送必须在消息接收之前)。

2、Java内存模型抽象

在Java中,所有的实例域,静态域,数据元素等共享变量都存储在堆内存中,对内存在线程之间共享。而局部变量,方法定义参数和异常处理参数等是不会在线程之间共享的(不存在内存可见性问题,也不受内存模型)。

Java内存模型(JMM)决定了一个线程对共享变量的写入合适对另外一个线程可见。,控制着Java线程之间的通讯。JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化),本地内存中存储了该线程以读/写共享变量的副本。JMM通过控制主内存与每个线程的本地内存之间的交互,来为java程序员提供内存可见性保证。

Java内存模型简析

现代的处理器使用写缓冲区来临时保存向内存写入的数据。写缓冲区可以保证指令流水线持续运行,它可以避免由于处理器停顿下来等待向内存写入数据而产生的延迟。同时,通过以批处理的方式刷新写缓冲区,以及合并写缓冲区中对同一内存地址的多次写,可以减少对内存总线的占用。虽然写缓冲区有这么多好处,但每个处理器上的写缓冲区,仅仅对它所在的处理器可见。这个特性会对内存操作的执行顺序产生重要的影响:处理器对内存的读/写操作的执行顺序,不一定与内存实际发生的读/写操作顺序一致!

3、重排序

在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。重排序分三种类型:

a) 编译器优化重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。

b) 指令级并行重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。

c) 内存系统重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

JMM属于语言级的内存模型,它确保在不同的编译器和不同的处理器平台之上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。

在单线程程序中,对存在控制依赖的操作重排序,不会改变执行结果(这也是as-if-serial语义允许对存在控制依赖的操作做重排序的原因);但在多线程程序中,对存在控制依赖的操作重排序,可能会改变程序的执行结果。

4、顺序一致性内存模型

顺序一致性为程序员提供了极强的内存可见性保证:①一个线程中的所有操作必须按照程序的顺序来执行。②(不管程序是否同步)所有线程都只能看到一个单一的操作执行顺序。在顺序一致性内存模型中,每个操作都必须原子执行且立刻对所有线程可见。

Java内存模型简析

  在概念上,顺序一致性模型有一个单一的全局内存,这个内存通过一个左右摆动的开关可以连接到任意一个线程。同时,每一个线程必须按程序的顺序来执行内存读/写操作。从上图我们可以看出,在任意时间点最多只能有一个线程可以连接到内存。当多个线程并发执行时,图中的开关装置能把所有线程的所有内存读/写操作串行化。

5、关于volatile

  当我们声明共享变量为volatile后,对这个变量的读/写将会很特别。理解volatile特性的一个好方法是:把对volatile变量的单个读/写,看成是使用同一个监视器锁对这些单个读/写操作做了同步。当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存;当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。通过volatile可以解决内存可见性的问题。

6、关于锁

  锁是java并发编程中最重要的同步机制。锁除了让临界区互斥执行外,还可以让释放锁的线程向获取同一个锁的线程发送消息。当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须要从主内存中去读取共享变量。通过锁不仅仅可以保证代码执行的原子性,还可以解决内存可见性的问题。