JMM-java线程内存模型

  

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

 

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

 

  计算机中的内存模型

    物理计算机中的并发问题与虚拟中的情况有很大的相似之处,物理机对并发的处理方案对于虚拟机的实现也有相当大的参考意义。

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

 

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

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

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

    上图中L1、L2、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)

posted @ 2019-07-05 20:28  fn-f  阅读(179)  评论(0编辑  收藏  举报