Java多线程内存模型

JMM的基本概念

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

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

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

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

典型的高并发引发的问题就存在因为线程读取到的数据尚未从另外的线程刷新到主内存中而引发的数据不一致问题。架构

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

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

1. lock:把主内存变量标识为一条线程独占,此时不容许其余线程对此变量进行读写jvm

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关键字不能保证原子性。


针对于上面所涉及到的知识点我总结出了有1到5年开发经验的程序员在面试中涉及到的绝大部分架构面试题及答案作成了文档和架构视频资料免费分享给你们(包括Dubbo、Redis、Netty、zookeeper、Spring cloud、分布式、高并发等架构技术资料),但愿能帮助到您面试前的复习且找到一个好的工做,也节省你们在网上搜索资料的时间来学习,也能够关注我一下之后会有更多干货分享。

资料获取方式: QQ群搜索“708-701-457” 便可免费领取



相关文章
相关标签/搜索