JMM-java线程内存模型

  

  怎么说呢?,最近看《深刻理解Java虚拟机 —— JVM高级特性与最佳实践》这本书,这本书中也介绍可关于Java内存模型与线程、锁等的问题,本篇就在此书之上作部分拓展,以便清楚的了解Java线程内存模型这个东东...java

 

  经过上篇,你们了解到关于JVM这个东西,这章节主要介绍了其内存模型,内部的垃圾收集算法,在接下来会讲的,请静待... 本篇主要是介绍JMM - Java线程内存模型算法

 

  计算机中的内存模型缓存

    物理计算机中的并发问题与虚拟中的状况有很大的类似之处,物理机对并发的处理方案对于虚拟机的实现也有至关大的参考意义。安全

    在现代计算机中,绝大多数的运算任务都须要靠处理器与内存交互实现,但交互时产生的 I/O 操做时间是没法避免的,且 I/O 是很是须要时间的,没法消除,如,读取运算数据、存储运算结果等。现代计算机中cpu的指令速度远超内存的存取速度,计算机的存储设备与处理器的运算速度有几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽量接近处理器运算速度的高速缓存(Cache)来做为内存与处理器之间的缓冲,将运算须要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中,这样处理器就无须等待缓慢的内存读写了。 多线程

 

    基于Cache高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,但也带来了更高的复杂度,一个新的问题:缓存一致性 并发

    多处理器系统中,每一个处理器都有本身的高速缓存,而它们又共享同一主内存(Main Memory)。当多个处理器的运算任务都涉及同一块主内存区域时,将可能致使各自的缓存数据不一致,那同步回到主内存时以谁的缓存数据为准呢?app

    为了解决一致性的问题,须要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议来进行操做,这类协议有MSI、MESI(Illinois Protocol)、MOSI、Synapse、Firefly及Dragon Protocol等。框架

    上图中L一、L二、L3表明多级高速缓存,提供了数据的访问性能,也减轻了数据总线上数据传输的压力,同时也带来了不少新的挑战,Memory表示主内存。工具

 

    Java线程内存模型(JMM)性能

       不一样的物理机器拥有不同的内存模型,而Java虚拟机也有本身的内存模型。

      JMM是一系列的Java虚拟机平台对开发者提供的多线程环境下的内存可见性、是否能够重排序等问题的无关具体平台的统一的保证。

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

      注:JSR-133,Java Memory Model and Thread specification revision,Java内存模型与线程规范修订。

 

      从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每一个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其余的硬件和编译器优化。

      Java语言规范中提到过,JVM中存在一个主存区(Main Memory或Java Heap Memory),Java中全部变量都是存在主存中的,对于全部线程进行共享,而每一个线程又存在本身的工做内存(Working Memory),工做内存中保存的是主存中某些变量的拷贝(副本),线程对全部变量的操做并不是发生在主存区,而是发生在工做内存中,而线程之间是不能直接相互访问,变量在程序中的传递,是依赖主存来完成的。

 

      疑问:副本拷贝?若线程中访问一个超大的对象,那也会将其进行拷贝吗?

       答:不会拷贝对象的,该对象的引用、对象中某个线程访问的字段有可能进行拷贝,可是不会把整个对象进行拷贝的。

 

       注:JMM是一个抽象的概念,而JVM也存在部分抽象概念,所以JMM与JVM是不存在映射关系的,若是强扯上关系,那就以本身理解为主。Java内存模型只是抽象出来的,与物理内存的对应关系在实际运行中,主内存和工做内存可能都处于物理机的主存中。

 

    内存间交互操做

       那Java 线程之间的对象在内存中如何进行操做呢?(参考深刻《理解Java虚拟机》)

 

      关于主内存与工做内存之间的具体交互,即一个变量如何从主内存拷贝到工做内存? 又如何从工做内存同步到主内存呢?

      内存交互操做有8种,虚拟机实现必须保证每个操做都是原子的,不可在分的(对于double和long类型的变量来讲,load、store、read和write操做在某些平台上容许例外)

              lock     (锁定):做用于主内存的变量,把一个变量标识为线程独占状态;
              unlock (解锁):做用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才能够被其余线程锁定;
              read    (读取):做用于主内存变量,它把一个变量的值从主内存传输到线程的工做内存中,以便随后的load动做使用;
              load     (载入):做用于工做内存的变量,它把read操做从主存中变量放入工做内存中;
              use      (使用):做用于工做内存中的变量,它把工做内存中的变量传输给执行引擎,每当虚拟机遇到一个须要使用到变量的值,就会使用到这个指令;
              assign  (赋值):做用于工做内存中的变量,它把一个从执行引擎中接受到的值放入工做内存的变量副本中;
              store    (存储):做用于主内存中的变量,它把一个从工做内存中一个变量的值传送到主内存中,以便后续的write使用;
              write  (写入):做用于主内存中的变量,它把store操做从工做内存中获得的变量的值放入主内存的变量中;

 

      若是要把一个变量从主内存赋值到工做内存,须要顺序的执行read  ---> load操做;      若是要把变量从工做内存同步到主内存中,则须要顺序的执行store --->write操做;

      注:---> 只表明顺序,该符号不保证连续性,中间能够有其它执行操做。

      JMM对这八种指令的使用,制定了以下规则:

              不容许read和load、store和write操做之一单独出现。即便用了read必须load,使用了store必须write;
              不容许线程丢弃他最近的assign操做,即工做变量的数据改变了以后,必须告知主存;
              不容许一个线程将没有assign的数据从工做内存同步回主内存;
              一个新的变量必须在主内存中诞生,不容许工做内存直接使用一个未被初始化的变量。就是怼变量实施use、store操做以前,必须通过assign和load操做;
              一个变量同一时间只有一个线程能对其进行lock。屡次lock后,必须执行相同次数的unlock才能解锁;
              若是对一个变量进行lock操做,会清空全部工做内存中此变量的值,在执行引擎使用这个变量前,必须从新load或assign操做初始化变量的值;
              若是一个变量没有被lock,就不能对其进行unlock操做。也不能unlock一个被其余线程锁住的变量;
              对一个变量进行unlock操做以前,必须把此变量同步回主内存;

      JMM对这八种操做规则和对volatile的一些特殊规则就能肯定哪里操做是线程安全,哪些操做是线程不安全的了。可是这些规则实在复杂,很难在实践中直接分析。因此通常咱们也不会经过上述规则进行分析。更多的时候,使用java的happen-before规则来进行分析。

 

    Java内存模型带来的问题  

      可见性问题:上面所述中有一个拷贝副本的操做,那某一线程将值修改后,如何进行同步到其余线程的值中呢?

      线程竞争问题:两个线程对某个值进行操做后,都会对主内存中的值进行从新赋值,那此时新的值结果并不是是准确的,如何保证线程执行的结果一致性呢?

      重排序问题:Java 内存模型还会对指令进行重排序操做,在执行程序时为了提升性能编译器和处理器常常会对指令进行重排序操做。



     Volatile修饰的特殊规则

       volatile关键字主要是Java虚拟机提供的最轻量级的同步机制。此时能够借助synchronized来配合使用。

      当一个变量被volatile修饰后,它将具有两个特性:可见性、禁止指令重排。(记住呀,这儿没有原子性的)

         可见性:假如在多线程的场景下,某一线程修改了变量的值,那么这个新的值对其余全部线程来讲是当即得知的。

         禁止指令重排:在程序运行过程当中,只保证结果最终的一致性,但在编译时,会对代码进行优化,而此时不知足 '先行发生' 原则,编译器会自行进行排序优化,而volatile关键字禁止指令重排,保证有序性。

          具体实现:(这儿参考《深刻理解Java虚拟机》,查看对应的字节码文件,未找到相关明显的区别,除了volatile),《深刻理解Java虚拟机》中有一句话:“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”,lock前缀指令生成一个内存屏障(Memory Banrrier)保证重排序后的指令不会越过内存屏障,实现对内存操做的顺序限制,即volatile以前的代码只会在volatile以前执行,volaiter以后的代码只会在volatile以后执行。 

          注:Lock前缀的做用:使本CPU的Cache写入内存,该写入动做也会引发别的CPU或别的内核无效化,好比以前的多线程对num进行++操做;同时指令重排序没法越过内存屏障,保证指令的有序。

 

      上面提到保证有序性,除了volatile以外,还有synchronized、final等关键字。

      Volatile的使用场景之一是:在DCL(DOuble-Check-Lock)双重校验锁的单例对象建立其实是一种延迟初始化的技巧,为建立的对象的变量使用volatile来修饰,保证线程之间的该对象的可见性。

        

      什么是内存屏障呢?如何实现?

        待完善...

 

    先行发生原则

      常规的开发中,判断数据是否存在竞争,线程是否安全,都须要依据Happens-Before原则,也就是Java内存模型当中定义的两项操做之间的偏序关系。意思就是当A操做先行发生于B操做,则在发生B操做的时候,操做A产生的影响能被B观察到,“影响”包括修改了内存中的共享变量的值、发送了消息、调用了方法等。

      Happen-Before的规则有如下几条:

              程序次序规则(Program Order Rule):在一个线程内,程序的执行规则跟程序的书写规则是一致的,从上往下执行。
              管程锁定规则(Monitor Lock Rule):一个Unlock的操做确定先于下一次Lock的操做。这里必须是同一个锁。同理咱们能够认为在synchronized同步同一个锁的时候,锁内先行执行的代码,对后续同步该锁的线程来讲是彻底可见的。
              volatile变量规则(volatile Variable Rule):对同一个volatile的变量,先行发生的写操做,确定早于后续发生的读操做
              线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的没一个动做
              线程停止规则(Thread Termination Rule):Thread对象的停止检测(如:Thread.join(),Thread.isAlive()等)操做,必行晚于线程中全部操做
              线程中断规则(Thread Interruption Rule):对线程的interruption()调用,先于被调用的线程检测中断事件(Thread.interrupted())的发生
              对象停止规则(Finalizer Rule):一个对象的初始化方法先于一个方法执行Finalizer()方法
              传递性(Transitivity):若是操做A先于操做B、操做B先于操做C,则操做A先于操做C

      以上就是Happen-Before中的规则,Java无需任何同步手段保障,就能够成立的先行发生原则就是上面的几个了。经过这些条件的断定,仍然很难判断一个线程是否能安全执行,毕竟在咱们的时候线程安全多数依赖于工具类的安全性来保证。想提升本身对线程是否安全的判断能力,必然须要理解所使用的框架或者工具的实现,并积累线程安全的经验。

 

      那一个操做,在 '时间' 上先发生,可否说这个操做是 '先行发生' 呢?   再者,一个操做是 '先行发生' ,那可否说这个操做是 '时间上先发生' 呢?

      答案就是不,两个都是不能够。时间前后顺序与先行发生原则之间基本没有太大关系,衡量并发线程是否安全问题,一切以先行发生原则为准。

 

 

  (愿你的每一行代码,都有让世界进步的力量    ------   fn)

相关文章
相关标签/搜索