在并发编程中,需要注意两个关键问题:

线程间如何通信? 即: 线程间以何种机制来交换信息.

线程间如何同步? 即: 线程间以何种机制来控制不同线程间操作发生的相对顺序.

在命令式编程中,线程之间的通信机制有两种:共享内存和消息传递。

两种内存模型比较

模型 如何通信 如何同步
共享内存 线程之间共享内存的公共状态,通过写-读内存中的公共状态进行隐式的通信。 同步是显式进行的。必须显式指定某个方法或某段代码需要在线程之间互斥执行。
消息传递 线程之间没有公共状态,线程之间必须通过发送消息来显式的通信。 由于消息的发送必须在消息接收之前,因此同步是隐式执行的。

Java的并发采用的是共享内存模型,它的线程之间的通信总是隐式执行的。

Java内存模型的抽象结构

运行时内存的划分

先看下运行时数据区:

Java运行时的数据区

堆在线程间是共享的,而所有的实例域、静态域和数组元素等变量都存储在堆内存(这些元素统一被称为共享变量)。而栈中的变量(局部变量、方法定义的参数、异常处理器的参数等)不会在线程之间共享,也就不会存在 内存可见性 。内存可见性针对的是共享变量。

堆的内存不可见问题

现代计算机为了高效,会在高速缓存区中缓存共享变量,线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,它存储了该线程以读、写共享变量的拷贝(副本)。本地内存是JMM的一个抽象的概念,并不真实存在。

Java内存模型的抽象结构示意图

从图中可以看出:

  • 所有的共享变量都存储在主内存中。
  • 每个线程都保留了一份该线程使用到了的共享变量的拷贝。
  • 如果A与B两个线程进行通信的话,需要经过以下两个步骤:
    • A将本地内存中被更新过的共享变量副本刷新到主内存中。
    • B需要到主内存中去读取A之前已经更新过的共享变量。

整个过程中A、B是无法之间访问对方的工作内存的,线程之间的通信必须经过主内存

注意:根据JMM的规定,线程对共享变量的所有操作都必须在自己的本地内存中进行,不能直接从主内存中读取。

因此B并不是直接到主内存读取共享变量的值,而是先在本地内存中找到这个共享变量,发现这个共享变量已经被更新了,然后B去主内存中读取这个共享变量的新值,并拷贝到B的本地内存中,最后B在从本地内存读取新值。

JMM正是通过控制主内存与每个线程的本地内存之间的交互来为Java程序员提供内存可见性的保证。

Java中的volatile关键字可以保证多线程操作共享变量的可见性以及禁止指令重排序,synchronized关键字不仅可以保证可见性,同时也可以保证原子性。

指令的重排序

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

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

2、指令级并行的重排序:多条指令不存在数据依赖时,处理器可以改变语句对应机器指令的执行顺序。

3、内存系统的重排序。

从源码到最终的执行序列会按上述顺序进行重排序。

这些重排序很可能导致多线程程序出现内存可见性的问题。

对于编译器,JMM规定会禁止特定类型的编译器重排序

对于处理器,JMM规定**在生成指令序列时,插入特定类型的内存屏障(Memory Barriers)指令,通过它来禁止。

内存屏障的类型

happens-before

Java内存模型使用happens-before的概念来描述操作之间的内存可见性。如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系。

  • 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
  • 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
  • volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
  • 传递性:如果A happens-before B且B happens-before C,那么A happens-before C。
  • start规则:如果线程A执⾏操作ThreadB.start()启动线程B,那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
  • join规则:如果线程A执⾏操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
两个操作之间具有happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行!happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前(the first is visible to and ordered before the second)。  

通俗的讲,happens-before可以理解为发生在某某之前,其主要是为了体现程序执行的顺序性,而这种顺序性保证了JMM的内存可见性。

JMM与Java内存区域

区别

两者是不同的概念层次,JMM是抽象的,是用来描述一组规则,通过这个规则实现了线程的通信和同步。

而Java运行时的内存的划分是具象的,是JVM运行Java程序时,必要的内存划分。

联系

都存在私有的数据区域和共享数据区域。

  • JMM中主内存属于共享数据区域,包含堆和方法区。
  • JMM中本地内存属于私有数据区域,包含了程序计数器、本地方法区、虚拟机栈。

评论