Java多线程内存模型

JMM的基本概念

Java作为平台无关性语言,JLS(Java语言规范)定义了一个统一的内存管理模型JMM(Java Memory Model)。JMM规定了jvm内存分为主内存和工作内存 ,主内存存放程序中所有的类实例、静态数据等变量,是多个线程共享的,而工作内存存放的是该线程从主内存中拷贝过来的变量以及访问方法所取得的局部变量,是每个线程私有的其他线程不能访问。每个线程对变量的操作都是以先从主内存将其拷贝到工作内存再对其进行操作的方式进行,多个线程之间不能直接互相传递数据通信,只能通过共享变量来进行。

从上图来看,线程1与线程2之间如要通信的话,必须要经历下面2个步骤:

1. 首先,线程1把本地工作内存中更新过的共享变量刷新到主内存中去。

2. 然后,线程2到主内存中去读取线程1之前已更新过的共享变量。

典型的高并发引起的问题就存在由于线程读取到的数据还没有从另外的线程刷新到主内存中而引起的数据不一致问题。

主内存与工作内存的数据交互

JLS一共定义了8种操作来完成主内存与线程工作内存的数据交互:

  1. lock:把主内存变量标识为一条线程独占,此时不允许其他线程对此变量进行读写
  2. unlock:解锁一个主内存变量
  3. read:把一个主内存变量值读入到线程的工作内存
  4. load:把read到变量值保存到线程工作内存中作为变量副本
  5. use:线程执行期间,把工作内存中的变量值传给字节码执行引擎
  6. assign:字节码执行引擎把运算结果传回工作内存,赋值给工作内存中的结果变量
  7. store:把工作内存中的变量值传送到主内存
  8. write:把store传送进来的变量值写入主内存的变量中

使用标准的操作再来重现一下上方的2个线程之间的交互流程则是这样的:

  1. 线程1从主内存read一个值为0的变量x到工作内存
  2. 使用load把变量x保存到工作内存作为变量副本
  3. 将变量副本x使用use传递给字节码执行引擎进行x++操作
  4. 字节码执行引擎操作完毕后使用assign将结果赋值给变量副本
  5. 使用store把变量副本传送到主内存
  6. 使用write把store传送的数据写到主内存
  7. 线程2从主内存read到x,然后load-->use-->assign-->store-->write

另外使用这8种操作也有一些规则:

  1. read 和 load必须以组合的方式出现,不允许一个变量从主内存读取了但工作内存不接受情况出现
  2. store和write必须以组合的方式出现,不允许从工作内存发起了存储操作但主内存不接受的情况出现
  3. 工作内存的变量如果没有经过 assign 操作,不允许将此变量同步到主内存中
  4. 在 use 操作之前,必须经过 load 操作
  5. 在 store 操作之前,必须经过 assign 操作
  6. unlock 操作只能作用于被 lock 操作锁定的变量
  7. 一个变量被执行了多少次 lock 操作就要执行多少次 unlock 才能解锁
  8. 一个变量只能在同一时刻被一条线程进行 lock 操作
  9. 执行 lock 操作后,工作内存的变量的值会被清空,需要重新执行 load 或 assign 操作初始化变量的值
  10. 对一个变量执行 unlock 操作之前,必须先把此变量同步回主内存中

多线程中的原子性、可见性、有序性

  1. 原子性:关于原子性的定义可以参考我的上篇博客《浅谈数据库事务》。在JLS中保证原子性的操作包括read、load、assign、use、store和write。基本数据类型(除了long 和double)操作都具有原子性。 如果需要更大范围的原子性操作的时候,可以使用lock和unlock操作来完成这种需求。
  2. 可见性:是指当一个线程修改了共享变量的值,其他线程是否能够立即得知这个修改。 由上方JMM的概念得知,线程操作数据是在工作内存的,当多个线程操作同一个数据的时候很容易读取到还没有被write到主内存变量的值。 Java是如何保证可见性的:volatile、synchronized、final关键字
  3. 有序性:在并发时,程序的执行可能会出现乱序。给人的直观感觉就是:写在前面的代码,会在后面执行。有序性问题的原因是因为程序在执行时,可能会进行指令重排,重排后的指令与原指令的顺序未必一致。关于指令重排会在下方讲。

指令重排

    int a=1;    int b=2;    int c=3;    int d=4;

你能说出上方这段代码的执行顺序么?其实我们可能理所当然的以为它会从上往下顺序执行。事实上,在实际运行时,为了优化指令的执行顺序等,代码指令可能并不是严格按照代码语句顺序执行的。上方的代码执行顺序可能完全反过来,这个就是指令重排。

不过呢,指令重排也不是可以随意重排的,它需要遵守一定的规则:

  1. 程序顺序规则:一个线程内保证语义的正确性。
  2. 锁规则:解锁肯定先于随后的加锁前。
  3. volatile规则:对一个volatile的写,先于volatile的读。
  4. 传递性:如果A 先于 B,且B 先于 C,那么A 肯定先于 C。
  5. start()规则:线程的start()操作先于线程的其他操作。
  6. join()规则:线程的所有操作先于线程的关闭。
  7. 程序中断规则:线程的中断先于被中断后执行的代码。
  8. 对象finalize规则:一个对象的初始化完成先于finalize()方法。

volatile关键字

volatile关键字旨在告诉虚拟机在这个地方要注意不能随意的进行指令重排,而虚拟机看到一个变量被volatile修饰以后就会采用一些特殊的手段来保证变量的可见性。不过要注意的是volatile关键字不能保证原子性。

原文发布于微信公众号 - Java学习录(Javaxuexilu)

原文发表时间:2019-03-21

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券