java内存模型JMM理解整理

  什么是JMM

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

  内存划分

  JMM规定了内存主要划分为主内存和工做内存两种。此处的主内存和工做内存跟JVM内存划分(堆、栈、方法区)是在不一样的层次上进行的,若是非要对应起来,主内存对应的是Java堆中的对象实例部分,工做内存对应的是栈中的部分区域,从更底层的来讲,主内存对应的是硬件的物理内存,工做内存对应的是寄存器和高速缓存。java

  JVM在设计时候考虑到,若是JAVA线程每次读取和写入变量都直接操做主内存,对性能影响比较大,因此每条线程拥有各自的工做内存,工做内存中的变量是主内存中的一份拷贝,线程对变量的读取和写入,直接在工做内存中操做,而不能直接去操做主内存中的变量。可是这样就会出现一个问题,当一个线程修改了本身工做内存中变量,对其余线程是不可见的,会致使线程不安全的问题。由于JMM制定了一套标准来保证开发者在编写多线程程序的时候,可以控制何时内存会被同步给其余线程。程序员

  内存交互操做

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

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

  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规则来进行分析。多线程

  模型特征

  原子性:例如上面八项操做,在操做系统里面是不可分割的单元。被synchronized关键字或其余锁包裹起来的操做也能够认为是原子的。从一个线程观察另一个线程的时候,看到的都是一个个原子性的操做。并发

1         synchronized (this) {
2             a=1;
3             b=2;
4         }

  例如一个线程观察另一个线程执行上面的代码,只能看到a、b都被赋值成功结果,或者a、b都还没有被赋值的结果。app

  可见性:每一个工做线程都有本身的工做内存,因此当某个线程修改完某个变量以后,在其余的线程中,未必能观察到该变量已经被修改。volatile关键字要求被修改以后的变量要求当即更新到主内存,每次使用前从主内存处进行读取。所以volatile能够保证可见性。除了volatile之外,synchronized和final也能实现可见性。synchronized保证unlock以前必须先把变量刷新回主内存。final修饰的字段在构造器中一旦完成初始化,而且构造器没有this逸出,那么其余线程就能看到final字段的值。
框架

  有序性:java的有序性跟线程相关。若是在线程内部观察,会发现当前线程的一切操做都是有序的。若是在线程的外部来观察的话,会发现线程的全部操做都是无序的。由于JMM的工做内存和主内存之间存在延迟,并且java会对一些指令进行从新排序。volatile和synchronized能够保证程序的有序性,不少程序员只理解这两个关键字的执行互斥,而没有很好的理解到volatile和synchronized也能保证指令不进行重排序。工具

  Volatile内存语义

   volatile的一些特殊规则

  Final域的内存语义

  被final修饰的变量,相比普通变量,内存语义有一些不一样。具体以下:

    • JMM禁止把Final域的写重排序到构造器的外部。
    • 在一个线程中,初次读该对象和读该对象下的Final域,JMM禁止处理器从新排序这两个操做。
 1 public class FinalConstructor {
 2 
 3     final int a;
 4 
 5     int b;
 6 
 7     static FinalConstructor finalConstructor;
 8 
 9     public FinalConstructor() {
10         a = 1;
11         b = 2;
12     }
13 
14     public static void write() {
15         finalConstructor = new FinalConstructor();
16     }
17 
18     public static void read() {
19         FinalConstructor constructor = finalConstructor;
20         int A = constructor.a;
21         int B = constructor.b;
22     }
23 }

  假设如今有线程A执行FinalConstructor.write()方法,线程B执行FinalConstructor.read()方法。

  对应上述的Final的第一条规则,由于JMM禁止把Final域的写重排序到构造器的外部,而对普通变量没有这种限制,因此变量A=1,而变量B可能会等于2(构造完成),也有可能等于0(第11行代码被重排序到构造器的外部)。

   对应上述的Final的第二条规则,若是constructor的引用不为null,A必然为1,要么constructor为null,抛出空指针异常。保证读final域以前,必定会先读该对象的引用。可是普通对象就没有这种规则。

  (上述的Final规则反复测试,遗憾的是我并无能模拟出来普通变量不能正常构造的结果

  Happen-Before(先行发生规则)

  在常规的开发中,若是咱们经过上述规则来分析一个并发程序是否安全,估计脑袋会很疼。由于更多时候,咱们是分析一个并发程序是否安全,其实都依赖Happen-Before原则进行分析。Happen-Before被翻译成先行发生原则,意思就是当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中的规则。经过这些条件的断定,仍然很难判断一个线程是否能安全执行,毕竟在咱们的时候线程安全多数依赖于工具类的安全性来保证。想提升本身对线程是否安全的判断能力,必然须要理解所使用的框架或者工具的实现,并积累线程安全的经验。

相关文章
相关标签/搜索