并发编程JMM系列之基础!

来源 | 公众号 | Justin的后端书架

Java程序员在进行多线程开发时,并不需要关心线程间是如何通信的,这些对程序员本来来说完全是透明的,但是内存可见性问题很容易让我们困惑,今天我们就讲讲Java内存模型(JMM)相关知识点,首先我们先讲讲内存模型的一些基本概念,对内存模型有个大概的认识,让我们开始今天的并发之旅吧。

一、并发编程模型的关键问题

并发编程模型主要有两种模型:共享内存模型和消息传递模型,在这两种模型中都需要解决并发编程最关键的两个问题:

共享内存模型:

  • 线程之间通信:线程之间共享程序的公共状态,通过读-写内存中的公共状态,进行隐式通信;
  • 线程之间同步:程序员必须显式的指定某个方法或者代码块需要在线程间互斥执行,显式进行同步;

消息传递模型:

  • 线程之间通信:线程之间通过发送消息,进行显式通信;
  • 线程之间同步:消息的发送必须在消息的接收之前,隐式进行同步;

通信指的是线程之间以什么机制来交换信息;

同步指的是程序用于控制不同线程间操作发生相对顺序的机制;

Java的并发采用的是共享内存模型,Java之间线程通信是隐式进行的,因此我们要理解隐式进行的线程间通信机制,才能更好的掌握Java并发,下面我们就谈谈这种隐式的通信机制。

二、Java内存模型的抽象结构

在Java中,所有的实例域,静态域和数组元素都存储在堆内存中,堆内存在线程间是共享的,因此这些数据对象会受到内存模型的影响;

Java线程之间的通信就依赖于Java内存模型(JMM),JMM决定了一个线程对共享变量的写入何时对另一个线程可见;从抽象角度看,JMM定义了线程和主内存之间的抽象关系,如下图所示:

JMM抽象模型

:线程之间的共享变量存在主内存中,每个线程都有一个本地内存,用于存储主内存中共享变量的副本;

JMM线程通信过程

:线程A每次把本地内存A中更新过的共享变量刷新到主内存中,线程B到主内存中去读取线程A更新后的共享变量,并刷新到本地内存B中;完成线程AB之间的通信。这个过程一定得经过主内存,所以JMM是通过控制主内存与每个线程的本地内存之间交互来提供内存可见性的保证机制。

三、源代码到指令序列重排序

为了提高程序执行的性能,编译器和处理器常常会对指令做重排序处理,整个过程如下:

四、处理器对内存操作的影响

现在常见的处理器都使用写缓冲区,首先我们看下使用写缓冲区有什么好处?再看看对内存操作有什么影响?

写缓冲区的优势:

写缓冲区临时保存线程向内存写入的数据,这样做可以避免处理器停下来等待向内存写入数据产生的延迟,通过批处理的方式刷新写缓冲区,合并写缓冲区中对同一内存地址的多次读写,从而减少了对内存总线的占用,提高了处理器处理数据的效率。

对内存执行顺序的影响:

处理器对内存读写操作顺序不一定与内存实际发生的读写顺序一致,可能造成数据结果的不一致。

怎么解决这个问题呢?

还是之前提到的内存屏障,Java编译器在生成指令时会在适当位置插入内存屏障指令来禁止特定类型的处理器重排序(执行顺序会影响执行结果的这种),JMM把内存屏障分为下面四种:

五、先行发生原则

在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作直接必须要存在先行发生原则(happens-before)。

先行发生原则规则如下:

程序次序规则:

同一个线程内,按照程序代码顺序,写在前面的操作先行发生于写在后面的操作;

管程锁定原则:

对于一个锁的解锁,先行发生于随后对这个锁的加锁;

volatile变量规则:

对一个volatile变量的写操作先行发生于任意后续对该变量的读操作;

线程启动规则:

Thread对象的start()方法先行发生于此线程的每一个动作;

线程终止规则:

线程的所有操作都先行发生于对此线程的终止检测,可以通过Thread.join()方法结束和Thread.isAlive()的返回值等手段检测到线程已经终止执行。

线程中断规则:

对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断时事件的发生。Thread.interrupted()可以检测是否有中断发生。

对象终结规则:

一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()的开始。

传递性:

如果操作A 先行发生于操作B,操作B 先行发生于操作C,那么可以得出A 先行发生于操作。

2