Java内存模型

1、Java内存模型(JMM)

Java内存模型的主要目标:定义在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节

注意:上边的变量指的是共享变量(实例字段、静态字段、数组对象元素),不包括线程私有变量(局部变量、方法参数),因为私有变量不会存在竞争关系。

2、内存模型

说明:

  • 所有共享变量存于主内存
  • 每一条线程都有自己的工作内存(就是上图所说的本地内存)
  • 工作内存中保存了被该线程使用到的变量的主内存副本

注意:

  • 线程对变量的操作都要在工作内存中进行,不能直接操作主内存
  • 不同的线程之间无法直接访问对方的工作内存中的变量
  • 不同线程之间的变量的传递必须通过主内存

类比:(注意:主内存与工作内存只是一个概念,与堆栈内存没有关系,下边的类比只是帮助理解)

  • 主内存:对应于Java堆中的对象实例数据部分(注意:堆中还保存了对象的其他信息,eg.Mark Word、class Point和用于字节对其补白的填充数据)
  • 工作内存:对应于栈中的部分区域

内存屏障指令:

  • lock:作用于主内存,把一个变量标识为一条线程独占的状态
  • unlock:作用于主内存,把一个处于锁定的变量解锁

下边四条是与volatile实现内存可见性直接相关的四条(store、write、read、load)

  • store:把工作内存中的变量的值传送到主内存中
  • write:把store操作从工作内存中得到的变量值放入到主内存的变量中
  • read:把一个变量的值从主内存中传输到线程的工作内存
  • load:把read操作从主内存中获取到的变量值放入工作内存的变量中去

注意:

  • 一个变量在同一时刻只允许一条线程对其进行lock操作
  • lock操作会将该变量在所有线程工作内存中的变量副本清空,否则就起不到锁的作用
  • lock操作可被同一条线程多次进行,lock几次,就要unlock几次(可重入锁)
  • unlock之前必须先执行store-write
  • store-write必须成对出现(工作内存-->主内存)
  • read-load必须成对出现(主内存-->工作内存)

变量对所有线程的可见性

     可见性:线程1对共享变量的修改能及时被线程2看到

2.1、共享变量不可见的原因

  • 共享变量更新后的值没有在工作内存和主内存之间及时更新
  • 线程交错执行
  • 指令重排序结合线程交错执行

2.2、实现共享变量及时更新的措施

     线程1修改过共享变量后,将共享变量刷到主内存,然后,线程2从主内存读取该共享变量,将该共享变量载入到工作内存中

    注意:在短时间内的高并发情况下,如果发生下列三种情况,则线程2就读不到线程1修改过的最新的值了,

  • 可能线程1根本来不及将修改过后的共享变量刷到主内存(这个时间非常短,但是还是有)的时候,线程2就已经读取了原有的主内存变量到其工作内存中。
  • 可能线程1虽然将修改过后的值刷到了主内存中,但是线程2的工作内存中的变量副本还没来得及从CPU刷新回来,所以线程2读取到的还是原来的工作内存中的变量副本
  • 可能线程1根本来不及将修改过后的共享变量刷到主内存的时候,同时,线程2的工作内存中的变量副本还没来得及从CPU刷新回来

    注意:工作内存中的变量副本在使用之后,不会立刻消失掉,会一直存在,这样其值也一直不变,直到对其进行写操作或数据从CPU中刷新回来(类比volatile-read的作用)。

   指令重排序:代码书写顺序与实际执行顺序不同(编译器或处理器为提高程序性能做的优化)

Java线程与硬件处理器

在Window系统和Linux系统上,Java线程的实现是基于一对一的线程模型,所谓的一对一模型,实际上就是通过语言级别层面程序去间接调用系统内核的线程模型,即在使用Java线程时,Java虚拟机内部是转而调用当前操作系统的内核线程来完成当前任务内核线程(Kernel-Level Thread,KLT),是由操作系统内核(Kernel)支持的线程,这种线程是由操作系统内核来完成线程切换,内核通过操作调度器进而对线程执行调度,并将线程的任务映射到各个处理器上。每个内核线程可以视为内核的一个分身,这也就是操作系统可以同时处理多任务的原因。由于我们编写的多线程程序属于语言层面的,程序一般不会直接去调用内核线程,取而代之的是一种轻量级的进程(Light Weight Process),也是通常意义上的线程,由于每个轻量级进程都会映射到一个内核线程,因此可以通过轻量级进程调用内核线程,进而由操作系统内核将任务映射到各个处理器,这种轻量级进程与内核线程间1对1的关系就称为一对一的线程模型,如下图 

 

如图所示,每个线程最终都会映射到CPU中进行处理,如果CPU存在多核,那么一个CPU将可以并行执行多个线程任务。

理解JMM中的happens-before 原则

倘若在程序开发中,仅靠sychronized和volatile关键字来保证原子性、可见性以及有序性,那么编写并发程序可能会显得十分麻烦,幸运的是,在Java内存模型中,还提供了happens-before 原则来辅助保证程序执行的原子性、可见性以及有序性的问题,它是判断数据是否存在竞争、线程是否安全的依据,happens-before 原则内容如下

  • 程序顺序原则,即在一个线程内必须保证语义串行性,也就是说按照代码顺序执行。

  • 锁规则   解锁(unlock)操作必然发生在后续的同一个锁的加锁(lock)之前,也就是说,如果对于一个锁解锁后,再加锁,那么加锁的动作必须在解锁动作之后(同一个锁)

  • volatile规则   volatile变量的写,先发生于读,这保证了volatile变量的可见性,简单的理解就是,volatile变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存,任何时刻,不同的线程总是能够看到该变量的最新值。

  • 线程启动规则 线程的start()方法先于它的每一个动作,即如果线程A在执行线程B的start方法之前修改了共享变量的值,那么当线程B执行start方法时,线程A对共享变量的修改对线程B可见

  • 传递性   A先于B ,B先于C 那么A必然先于C

  • 线程终止规则   线程的所有操作先于线程的终结,Thread.join()方法的作用是等待当前执行的线程终止。假设在线程B终止之前,修改了共享变量,线程A从线程B的join方法成功返回后,线程B对共享变量的修改将对线程A可见。

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

  • 对象终结规则 对象的构造函数执行,结束先于finalize()方法

 

参考:

 https://blog.csdn.net/javazejian/article/details/72772461(推荐)

http://www.cnblogs.com/java-zhao/p/5124725.html

https://blog.csdn.net/javazejian/article/details/72772461(推荐)

posted on 2018-09-25 19:33  溪水静幽  阅读(116)  评论(0)    收藏  举报