浅析java内存模型--JMM

在并发编程中,多个线程之间采起什么机制进行通讯(信息交换),什么机制进行数据的同步?程序员

在Java语言中,采用的是共享内存模型来实现多线程之间的信息交换和数据同步的。算法

线程之间经过共享程序公共的状态,经过读-写内存中公共状态的方式来进行隐式的通讯。同步指的是程序在控制多个线程之间执行程序的相对顺序的机制,在共享内存模型中,同步是显式的,程序员必须显式指定某个方法/代码块须要在多线程之间互斥执行。编程

在说Java内存模型以前,咱们先说一下Java的内存结构,也就是运行时的数据区域:数组

Java虚拟机在执行Java程序的过程当中,会把它管理的内存划分为几个不一样的数据区域,这些区域都有各自的用途、建立时间、销毁时间。缓存

clipboard.png

Java运行时数据区分为下面几个内存区域:安全

1.PC寄存器/程序计数器:性能优化

严格来讲是一个数据结构,用于保存当前正在执行的程序的内存地址,因为Java是支持多线程执行的,因此程序执行的轨迹不可能一直都是线性执行。当有多个线程交叉执行时,被中断的线程的程序当前执行到哪条内存地址必然要保存下来,以便用于被中断的线程恢复执行时再按照被中断时的指令地址继续执行下去。为了线程切换后能恢复到正确的执行位置,每一个线程都须要有一个独立的程序计数器,各个线程之间计数器互不影响,独立存储,咱们称这类内存区域为“线程私有”的内存,这在某种程度上有点相似于“ThreadLocal”,是线程安全的。数据结构

2.Java栈 Java Stack:多线程

Java栈老是与线程关联在一块儿的,每当建立一个线程,JVM就会为该线程建立对应的Java栈,在这个Java栈中又会包含多个栈帧(Stack Frame),这些栈帧是与每一个方法关联起来的,每运行一个方法就建立一个栈帧,每一个栈帧会含有一些局部变量、操做栈和方法返回值等信息。每当一个方法执行完成时,该栈帧就会弹出栈帧的元素做为这个方法的返回值,而且清除这个栈帧,Java栈的栈顶的栈帧就是当前正在执行的活动栈,也就是当前正在执行的方法,PC寄存器也会指向该地址。只有这个活动的栈帧的本地变量能够被操做栈使用,当在这个栈帧中调用另一个方法时,与之对应的一个新的栈帧被建立,这个新建立的栈帧被放到Java栈的栈顶,变为当前的活动栈。一样如今只有这个栈的本地变量才能被使用,当这个栈帧中全部指令都完成时,这个栈帧被移除Java栈,刚才的那个栈帧变为活动栈帧,前面栈帧的返回值变为这个栈帧的操做栈的一个操做数。架构

因为Java栈是与线程对应起来的,Java栈数据不是线程共有的,因此不须要关心其数据一致性,也不会存在同步锁的问题。

在Java虚拟机规范中,对这个区域规定了两种异常情况:若是线程请求的栈深度大于虚拟机所容许的深度,将抛出StackOverflowError异常;若是虚拟机能够动态扩展,若是扩展时没法申请到足够的内存,就会抛出OutOfMemoryError异常。在Hot Spot虚拟机中,可使用-Xss参数来设置栈的大小。栈的大小直接决定了函数调用的可达深度。

clipboard.png

3.堆 Heap:

堆是JVM所管理的内存中国最大的一块,是被全部Java线程锁共享的,不是线程安全的,在JVM启动时建立。堆是存储Java对象的地方,这一点Java虚拟机规范中描述是:全部的对象实例以及数组都要在堆上分配。Java堆是GC管理的主要区域,从内存回收的角度来看,因为如今GC基本都采用分代收集算法,因此Java堆还能够细分为:新生代和老年代;新生代再细致一点有Eden空间、From Survivor空间、To Survivor空间等。

4.方法区Method Area:

方法区存放了要加载的类的信息(名称、修饰符等)、类中的静态常量、类中定义为final类型的常量、类中的Field信息、类中的方法信息,当在程序中经过Class对象的getName.isInterface等方法来获取信息时,这些数据都来源于方法区。方法区是被Java线程锁共享的,不像Java堆中其余部分同样会频繁被GC回收,它存储的信息相对比较稳定,在必定条件下会被GC,当方法区要使用的内存超过其容许的大小时,会抛出OutOfMemory的错误信息。方法区也是堆中的一部分,就是咱们一般所说的Java堆中的永久区 Permanet Generation,大小能够经过参数来设置,能够经过-XX:PermSize指定初始值,-XX:MaxPermSize指定最大值。

5.常量池Constant Pool:

常量池自己是方法区中的一个数据结构。常量池中存储了如字符串、final变量值、类名和方法名常量。常量池在编译期间就被肯定,并保存在已编译的.class文件中。通常分为两类:字面量和应用量。字面量就是字符串、final变量等。类名和方法名属于引用量。引用量最多见的是在调用方法的时候,根据方法名找到方法的引用,并以此定为到函数体进行函数代码的执行。引用量包含:类和接口的权限定名、字段的名称和描述符,方法的名称和描述符。

6.本地方法栈Native Method Stack:

本地方法栈和Java栈所发挥的做用很是类似,区别不过是Java栈为JVM执行Java方法服务,而本地方法栈为JVM执行Native方法服务。本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常。

主内存和工做内存:

Java内存模型的主要目标是定义程序中各个变量的访问规则,即在JVM中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量与Java编程里面的变量有所不一样步,它包含了实例字段、静态字段和构成数组对象的元素,但不包含局部变量和方法参数,由于后者是线程私有的,不会共享,固然不存在数据竞争问题(若是局部变量是一个reference引用类型,它引用的对象在Java堆中可被各个线程共享,可是reference引用自己在Java栈的局部变量表中,是线程私有的)。为了得到较高的执行效能,Java内存模型并无限制执行引发使用处理器的特定寄存器或者缓存来和主内存进行交互,也没有限制即时编译器进行调整代码执行顺序这类优化措施。

JMM规定了全部的变量都存储在主内存(Main Memory)中。每一个线程还有本身的工做内存(Working Memory),线程的工做内存中保存了该线程使用到的变量的主内存的副本拷贝,线程对变量的全部操做(读取、赋值等)都必须在工做内存中进行,而不能直接读写主内存中的变量(volatile变量仍然有工做内存的拷贝,可是因为它特殊的操做顺序性规定,因此看起来如同直接在主内存中读写访问通常)。不一样的线程之间也没法直接访问对方工做内存中的变量,线程之间值的传递都须要经过主内存来完成。

clipboard.png

线程1和线程2要想进行数据的交换通常要经历下面的步骤:

1.线程1把工做内存1中的更新过的共享变量刷新到主内存中去。

2.线程2到主内存中去读取线程1刷新过的共享变量,而后copy一份到工做内存2中去。

Java内存模型是围绕着并发编程中原子性、可见性、有序性这三个特征来创建的,那咱们依次看一下这三个特征:

原子性(Atomicity):一个操做不能被打断,要么所有执行完毕,要么不执行。在这点上有点相似于事务操做,要么所有执行成功,要么回退到执行该操做以前的状态。

基本类型数据的访问大都是原子操做,long 和double类型的变量是64位,可是在32位JVM中,32位的JVM会将64位数据的读写操做分为2次32位的读写操做来进行,这就致使了long、double类型的变量在32位虚拟机中是非原子操做,数据有可能会被破坏,也就意味着多个线程在并发访问的时候是线程非安全的。

下面咱们来演示这个32位JVM下,对64位long类型的数据的访问的问题:

public class NotAtomicity {
//静态变量t
public  static long t = 0;
//静态变量t的get方法
public  static long getT() {
    return t;
}
//静态变量t的set方法
public  static void setT(long t) {
    NotAtomicity.t = t;
}
//改变变量t的线程
public static class ChangeT implements Runnable{
    private long to;
    public ChangeT(long to) {
        this.to = to;
    }
    public void run() {
        //不断的将long变量设值到 t中
        while (true) {
            NotAtomicity.setT(to);
            //将当前线程的执行时间片断让出去,以便由线程调度机制从新决定哪一个线程能够执行
            Thread.yield();
        }
    }
}
//读取变量t的线程,若读取的值和设置的值不一致,说明变量t的数据被破坏了,即线程不安全
public static class ReadT implements Runnable{
    public void run() {
        //不断的读取NotAtomicity的t的值
        while (true) {
            long tmp = NotAtomicity.getT();
            //比较是不是本身设值的其中一个
            if (tmp != 100L && tmp != 200L && tmp != -300L && tmp != -400L) {
                //程序若执行到这里,说明long类型变量t,其数据已经被破坏了
                System.out.println(tmp);
            }
            ////将当前线程的执行时间片断让出去,以便由线程调度机制从新决定哪一个线程能够执行
            Thread.yield();
        }
    }
}
public static void main(String[] args) {
    new Thread(new ChangeT(100L)).start();
    new Thread(new ChangeT(200L)).start();
    new Thread(new ChangeT(-300L)).start();
    new Thread(new ChangeT(-400L)).start();
    new Thread(new ReadT()).start();
}
}

在此我向你们推荐一个架构学习交流群。交流学习qun号:+q q-q u n:948 368 769里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多

咱们建立了4个线程来对long类型的变量t进行赋值,赋值分别为100,200,-300,-400,有一个线程负责读取变量t,若是正常的话,读取到的t的值应该是咱们赋值中的一个,可是在32的JVM中,事情会出乎预料。若是程序正常的话,咱们控制台不会有任何的输出,可实际上,程序一运行,控制台就输出了下面的信息:

-4294967096

4294966896

-4294967096

-4294967096

4294966896

之因此会出现上面的状况,是由于在32位JVM中,64位的long数据的读和写都不是原子操做,即不具备原子性,并发的时候相互干扰了。

32位的JVM中,要想保证对long、double类型数据的操做的原子性,能够对访问该数据的方法进行同步,就像下面的:

public class Atomicity {
//静态变量t
public  static long t = 0;
//静态变量t的get方法,同步方法
public synchronized static long getT() {
    return t;
}
//静态变量t的set方法,同步方法
public synchronized static void setT(long t) {
    Atomicity.t = t;
}
//改变变量t的线程
public static class ChangeT implements Runnable{
    private long to;
    public ChangeT(long to) {
        this.to = to;
    }
    public void run() {
        //不断的将long变量设值到 t中
        while (true) {
            Atomicity.setT(to);
            //将当前线程的执行时间片断让出去,以便由线程调度机制从新决定哪一个线程能够执行
            Thread.yield();
        }
    }
}
//读取变量t的线程,若读取的值和设置的值不一致,说明变量t的数据被破坏了,即线程不安全
public static class ReadT implements Runnable{
    public void run() {
        //不断的读取NotAtomicity的t的值
        while (true) {
            long tmp = Atomicity.getT();
            //比较是不是本身设值的其中一个
            if (tmp != 100L && tmp != 200L && tmp != -300L && tmp != -400L) {
                //程序若执行到这里,说明long类型变量t,其数据已经被破坏了
                System.out.println(tmp);
            }
            ////将当前线程的执行时间片断让出去,以便由线程调度机制从新决定哪一个线程能够执行
            Thread.yield();
        }
    }
}
public static void main(String[] args) {
    new Thread(new ChangeT(100L)).start();
    new Thread(new ChangeT(200L)).start();
    new Thread(new ChangeT(-300L)).start();
    new Thread(new ChangeT(-400L)).start();
    new Thread(new ReadT()).start();
}
}

在此我向你们推荐一个架构学习交流群。交流学习qun号:+q q-q u n:948 368 769里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多
这样作的话,能够保证对64位数据操做的原子性。

可见性:一个线程对共享变量作了修改以后,其余的线程当即可以看到(感知到)该变量这种修改(变化)。

Java内存模型是经过将在工做内存中的变量修改后的值同步到主内存,在读取变量前从主内存刷新最新值到工做内存中,这种依赖主内存的方式来实现可见性的。

不管是普通变量仍是volatile变量都是如此,区别在于:volatile的特殊规则保证了volatile变量值修改后的新值马上同步到主内存,每次使用volatile变量前当即从主内存中刷新,所以volatile保证了多线程之间的操做变量的可见性,而普通变量则不能保证这一点。

除了volatile关键字能实现可见性以外,还有synchronized,Lock,final也是能够的。

使用synchronized关键字,在同步方法/同步块开始时(Monitor Enter),使用共享变量时会从主内存中刷新变量值到工做内存中(即从主内存中读取最新值到线程私有的工做内存中),在同步方法/同步块结束时(Monitor Exit),会将工做内存中的变量值同步到主内存中去(即将线程私有的工做内存中的值写入到主内存进行同步)。

使用Lock接口的最经常使用的实现ReentrantLock(重入锁)来实现可见性:当咱们在方法的开始位置执行lock.lock()方法,这和synchronized开始位置(Monitor Enter)有相同的语义,即便用共享变量时会从主内存中刷新变量值到工做内存中(即从主内存中读取最新值到线程私有的工做内存中),在方法的最后finally块里执行lock.unlock()方法,和synchronized结束位置(Monitor Exit)有相同的语义,即会将工做内存中的变量值同步到主内存中去(即将线程私有的工做内存中的值写入到主内存进行同步)。

final关键字的可见性是指:被final修饰的变量,在构造函数数一旦初始化完成,而且在构造函数中并无把“this”的引用传递出去(“this”引用逃逸是很危险的,其余的线程极可能经过该引用访问到只“初始化一半”的对象),那么其余线程就能够看到final变量的值。

有序性:对于一个线程的代码而言,咱们老是觉得代码的执行是从前日后的,依次执行的。这么说不能说彻底不对,在单线程程序里,确实会这样执行;可是在多线程并发时,程序的执行就有可能出现乱序。用一句话能够总结为:在本线程内观察,操做都是有序的;若是在一个线程中观察另一个线程,全部的操做都是无序的。前半句是指“线程内表现为串行语义(WithIn Thread As-if-Serial Semantics)”,后半句是指“指令重排”现象和“工做内存和主内存同步延迟”现象。

Java提供了两个关键字volatile和synchronized来保证多线程之间操做的有序性,volatile关键字自己经过加入内存屏障来禁止指令的重排序,而synchronized关键字经过一个变量在同一时间只容许有一个线程对其进行加锁的规则来实现,

在单线程程序中,不会发生“指令重排”和“工做内存和主内存同步延迟”现象,只在多线程程序中出现。

happens-before原则:

Java内存模型中定义的两项操做之间的次序关系,若是说操做A先行发生于操做B,操做A产生的影响能被操做B观察到,“影响”包含了修改了内存中共享变量的值、发送了消息、调用了方法等。

下面是Java内存模型下一些”自然的“happens-before关系,这些happens-before关系无须任何同步器协助就已经存在,能够在编码中直接使用。若是两个操做之间的关系不在此列,而且没法从下列规则推导出来的话,它们就没有顺序性保障,虚拟机能够对它们进行随意地重排序。

a.程序次序规则(Pragram Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操做先行发生于书写在后面的操做。准确地说应该是控制流顺序而不是程序代码顺序,由于要考虑分支、循环结构。

b.管程锁定规则(Monitor Lock Rule):一个unlock操做先行发生于后面对同一个锁的lock操做。这里必须强调的是同一个锁,而”后面“是指时间上的前后顺序。

c.volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操做先行发生于后面对这个变量的读取操做,这里的”后面“一样指时间上的前后顺序。

d.线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每个动做。

e.线程终于规则(Thread Termination Rule):线程中的全部操做都先行发生于对此线程的终止检测,咱们能够经过Thread.join()方法结束,Thread.isAlive()的返回值等做段检测到线程已经终止执行。

f.线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,能够经过Thread.interrupted()方法检测是否有中断发生。

g.对象终结规则(Finalizer Rule):一个对象初始化完成(构造方法执行完成)先行发生于它的finalize()方法的开始。

g.传递性(Transitivity):若是操做A先行发生于操做B,操做B先行发生于操做C,那就能够得出操做A先行发生于操做C的结论。

一个操做”时间上的先发生“不表明这个操做会是”先行发生“,那若是一个操做”先行发生“是否就能推导出这个操做一定是”时间上的先发生 “呢?也是不成立的,一个典型的例子就是指令重排序。因此时间上的前后顺序与happens-before原则之间基本没有什么关系,因此衡量并发安全问题一切必须以happens-before 原则为准。

相关文章
相关标签/搜索