天天看点

Java多线程--内存模型(JMM)--详解

简介

        本文介绍Java的内存模型(JMM)。包括:JMM是什么,JMM结构,JMM的特性(线程安全型体现在哪些方面),synchronized的作用,volatile的作用。

JMM是什么

        JMM即为JAVA 内存模型(java memory model)。因为在不同的硬件生产商和不同的操作系统下,内存的访问逻辑有一定的差异,结果就是当你的代码在某个系统环境下运行良好,并且线程安全,但是换了个系统就出现各种问题。Java内存模型,就是为了屏蔽系统和硬件的差异,让一套代码在不同平台下能到达相同的访问结果。JMM从java 5开始的JSR-133发布后,已经成熟和完善起来。

        内存模型描述了程序中各个变量(实例域、静态域和数组元素)之间的关系,以及在实际计算机系统中将变量存储到内存和从内存中取出变量这样的底层细节

        Java Memory Model(Java内存模型), 围绕着在并发过程中如何处理可见性、原子性、有序性这三个特性而建立的模型。

JMM规定

  • 每个线程还有自己的工作内存(Working Memory),线程的工作内存中保存了该线程使用到的变量的主内存的副本拷贝,
  • 所有的变量都存储在主内存(Main Memory)中。
  • 线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量(首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后在将变量写回主内存)。(volatile变量仍然有工作内存的拷贝,但是由于它特殊的操作顺序性规定,所以看起来如同直接在主内存中读写访问一般)。
  • 不同的线程之间无法直接访问对方工作内存中的变量,线程之间值的传递都需要通过主内存来完成。

JMM结构

Java多线程--内存模型(JMM)--详解

线程A与线程B之间如要通信的话,必须要经历下面2个步骤:

  1. 线程A把本地内存A中更新过的共享变量刷新到主内存中去。
  2. 线程B到主内存中去读取线程A之前已更新过的共享变量。

JMM三个特性

JMM三个特性(线程安全性体现方面)

线程的安全性问题体现在:原子性、可见性、有序性。

体现方面 说明 导致原因 解决方法
原子性 一个或者多个操作在CPU执行的过程中不被中断的特性 线程切换

JDK Atomic开头的原子类(非阻塞CAS算法)

synchronized

LOCK

可见性 一个线程对共享变量的修改,另外一个线程能够立刻看到 缓存

synchronized

volatile

LOCK

有序性 程序执行的顺序按照代码的先后顺序执行 编译优化 Happens-Before规则

Happens-Before规则

参考网址:​​java内存模型以及happens-before规则 - 简书​​

1.规则项

规则 说明
程序顺序规则 一个线程中的每个操作,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()操作成功返回。
线程中断规则 对线程interrupt()的调用 happen—before 发生于被中断线程的代码检测到中断时事件的发生。
对象终结规则 就是一个对象的初始化的完成(构造函数执行的结束) happens-before它的finalize()方法

2. happens-before含义

        JMM(java 内存模型)可以通过happens-before关系向程序员提供跨线程的内存可见性保证(如果A线程的写操作a与B线程的读操作b之间存在happens-before关系,尽管a操作和b操作在不同的线程中执行,但JMM向程序员保证a操作将对b操作可见)。具体的定义为:

(1)如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。

(2)两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM允许这种重排序)。

上面的(1)是JMM对程序员的承诺。从程序员的角度来说,可以这样理解happens-before关系:如果A happens-before B,那么Java内存模型将向程序员保证——A操作的结果将对B可见,且A的执行顺序排在B之前。注意,这只是Java内存模型向程序员做出的保证!

上面的(2)是JMM对编译器和处理器重排序的约束原则。正如前面所言,JMM其实是在遵循一个基本原则:只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序),编译器和处理器怎么优化都行。JMM这么做的原因是:程序员对于这两个操作是否真的被重排序并不关心,程序员关心的是程序执行时的语义不能被改变(即执行结果不能被改变)。因此,happens-before关系本质上和as-if-serial语义是一回事。

3.happens-before存在的意义

        重排序原则有编译器重排序和处理器重排序等,如果让程序员再去了解这些底层的实现以及具体规则,那么程序员的负担就太重了,严重影响了并发编程的效率。因此,JMM为程序员在上层提供了六条规则,这样我们就可以根据规则去推论跨线程的内存可见性问题,而不用再去理解底层重排序的规则。

synchronized作用

说明

synchronized可保证原子性、有序性和可见性

一个线程执行互斥代码过程

1. 获得同步锁;

2. 清空工作内存;

3. 从主内存拷贝对象副本到工作内存;

4. 执行代码(计算或者输出等);

5. 刷新主内存数据;

6. 释放同步锁。

volatile作用

说明

保证有序性和可见性,不保证原子性。

可见性:一个变量被声明为volatile时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存。当其他线程读取该共享变量时,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值。

有序性:通过禁止指令重排的方式保证有序性

volatile流程

1)从主存读取volatile变量到本地副本

2)修改变量值

3)本地副本值写回主存

4)插入内存屏障,即lock指令。内存屏障会让其他线程每次读取强制从主存读取(让其他线程可见)

其他网址​