美文网首页并发
java并发(二)线程安全性与Java内存模型

java并发(二)线程安全性与Java内存模型

作者: 黄金矿工00七 | 来源:发表于2018-06-24 04:56 被阅读7次

    谈到并发,首先要提到的就是安全,所有的并发编程的前提都是安全。在java中,

    • 什么是线程安全性?
      • 在多线程并发访问一个对象或者类时,他始终都能表现出正确的行为,也就是说,我们在调用时不需要做额外的同步。
    • 线程安全的几个特点
      • 原子性
        原子性,通俗来说就是几个操作在操作过程中,是不可以被其他线程干扰,举例来说
      public class Demo {
      
        private int count = 0;
       //在并发环境下,多个线程同时执行以下方法,可能会发生不安全操作
        public void add() {
         ++count;
        }
      }
      
      因为++操作在java中是非原子的,它包含三个操作:读-改-写,那么在使用该类的时候我们就无法保证原子性
      • 可见性
        可见性简单来说就是当一个线程修改了某个共享变量的值,这个更新能够立刻被其他线程所知晓,具体的原理我们在JMM中详细说。
      • 有序性
        有序性,是保证线程内语义是串行的,禁止指令重排。举个例子
      int  a ;
      a = 5;
      a = 6;
      int  b = a;
      int c = a + b;
      
      当发生指令重排时,语句3先于2执行,则结果发生错误。尤其是在-server模式下,处于对性能的考虑,虚拟机会做出一些优化。
      但是指令重排是有原则的,一下情况不发生重排,我列举一下:
      • Happen-Benfore原则
        • 程序顺序原则:一个线程内语义串行性
        • volatile原则:volatile变量的写先发生于读,因此保证了可见性
        • 锁原则:解锁必然发生于随后的加锁前
        • 传递性:a先于b,b先于c,则a先于c
        • 线程的start方法先于他的每一个操作
        • 线程的所有操作先于线程的终结
        • 对象的构造函数执行、结束先于finalize方法
    java内存模型
    • 计算机内存模型
      我们知道java是一种可移植的语言,它是平台无关性的,所以这就要求JMM在任何平台下都能达到一致的内存访问效果。

      • 首先我们来了解一下在计算机中硬件中是如何实现并发的,在计算机中,IO操作的速度与CPU的速度是相差很多的,所以为了保证并发,在内存与处理器之间引入了高速缓存(Cache),他的作用就是,在运算中,将使用到的数据拷贝到缓存中,当运算结束后再从缓存同步到内存之中,这样就解决了IO与CPU之间的速度差异问题。
      • 但是现在都是多核CPU,每个处理器都有自己的缓存,但是他们又共享同一主内存,那么当他们的运算涉及到同一块主内存时,就带来了缓存不一致的问题,所以就有了缓存一致性协议,我以一张图来表示他们之间的关系。
      计算机硬件模型
    • java内存区域
      指的是java虚拟机在运行程序时管理的内存,JVM把它划分成几个区域


      Java虚拟机内存区域
      • 方法区(Method Area):方法区属于线程共享的内存区域,又称Non-Heap(非堆),主要用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,根据Java 虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError 异常。值得注意的是在方法区中存在一个叫运行时常量池(Runtime Constant Pool)的区域,它主要用于存放编译器生成的各种字面量和符号引用,这些内容将在类加载后存放到运行时常量池中,以便后续使用。
      • Java堆(Java Heap):Java 堆也是属于线程共享的内存区域,它在虚拟机启动时创建,是Java 虚拟机所管理的内存中最大的一块,主要用于存放对象实例,几乎所有的对象实例都在这里分配内存,注意Java 堆是垃圾收集器管理的主要区域,因此很多时候也被称做GC 堆,如果在堆中没有内存满足实例分配需求,并且堆也无法再扩展时,将会抛出OutOfMemoryError 异常。
      • 程序计数器(Program Counter Register):属于线程私有的数据区域,是一小块内存空间,主要代表当前线程所执行的字节码行号指示器。字节码解释器工作时,通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
      • 本地方法栈(Native Method Stacks):本地方法栈属于线程私有的数据区域,这部分主要与虚拟机用到的 Native 方法相关。
      • 虚拟机栈(Java Virtual Machine Stacks):属于线程私有的数据区域,与线程同时创建,总数与线程关联,代表Java方法执行的内存模型。每个方法执行时都会创建一个栈桢来存储方法的的变量表、操作数栈、动态链接方法、返回值、返回地址等信息。
    • java内存模型

      • 虚拟机规范中描述说Java内存模型的主要目标是定义程序中各个变量的访问规则, 即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。但是要注意的是,这里的变量不是通常意义上的变量,它包括了实例属性、类属性以及构成数组的元素,但是不包括局部变量与方法参数。为什么这么说呢?
        根据上面的Java内存区域划分我们可以知道,在虚拟机栈中有一张局部变量表,存放了编译期可知的各种基本数据类型(boolean、 byte、 char、 short、 int、 float、 long、double) 、 对象引用(reference类型, 它不等同于对象本身, 可能是一个指向对象起始地址的引用指针, 也可能是指向一个代表对象的句柄或其他与此对象相关的位置) 和returnAddress类型(指向了一条字节码指令的地址),通俗一点说,如果局部变量是基本类型变量,则直接把这个变量的值保存在该变量对应的内存中.如果局部变量是引用类型的变量,则这个变量里存放的就是地址(注意的是:如果局部变量是一个reference类型, 它引用的对象在Java堆中可被各个线程共享, 但是reference本身在Java栈的局部变量表中, 它是线程私有的。),它们是线程私有的。

    我给出一个例子,大家来看一下局部变量是否是线程安全的

    public class Demo {
    
      private int count = 0;
    
      public static void main(String[] args) {
        Demo d = new Demo();
        for (int i = 0; i < 5; i++) {
          new Thread(new Runnable() {
            @Override
            public void run() {
              d.safeAdd();
            }
          }).start();
        }
        for (int i = 0; i < 5; i++) {
          new Thread(new Runnable() {
            @Override
            public void run() {
              d.unSafeAdd();
            }
          }).start();
        }
      }
    
      public void unSafeAdd() {
        for (int i = 0; i < 10000; i++) {
          ++count;
        }
        System.out.println("成员变量:" + count);
      }
    
      public void safeAdd() {
        int count = 0;
        for (int i = 0; i < 10000; i++) {
          ++count;
        }
        System.out.println("局部变量:" + count);
      }
    }
    
    运行结果:
    局部变量:10000
    局部变量:10000
    局部变量:10000
    局部变量:10000
    局部变量:10000
    成员变量:10000
    成员变量:20472
    成员变量:29394
    成员变量:40341
    成员变量:43576
    

    好了,了解完上面的,我们通过一张图看一下JMM。


    JMM.png
    • 工作内存
      我们知道,jvm实际上是通过线程来执行程序的,在每个线程创建的时候,jvm会给它分配一个工作内存,从JMM来看,数据线程私有数据区域,从某个程度上讲则应该包括程序计数器、虚拟机栈以及本地方法栈。从计算机硬件来看,工作内存可能是Cache或者寄存器中。
    • 主内存
      主要存储的是Java实例对象,所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量还是方法中的本地变量(也称局部变量),当然也包括了共享的类信息、常量、静态变量。(对比上面的局部变量来思考一下)在JMM中主内存属于共享数据区域,从某个程度上讲应该包括了堆和方法区,从计算机硬件来看,主内存也就是我们的硬件内存。
    • 工作方式
      对于一个实例对象中的成员方法而言,如果方法中包含本地变量是基本数据类型,将直接存储在工作内存的帧栈结构中,但倘若本地变量是引用类型,那么该变量的引用会存储在功能内存的帧栈中,而对象实例将存储在主内存(共享数据区域,堆)中。但对于实例对象的成员变量,不管它是基本数据类型还是引用类型,都会被存储到堆区。至于static变量以及类本身相关信息将会存储在主内存中。需要注意的是,在主内存中的实例对象可以被多线程共享,倘若两个线程同时调用了同一个对象的同一个方法,那么两条线程会将要操作的数据拷贝一份到自己的工作内存中,执行完成操作后才同步到主内存。

    最后我们来总结一下,JMM、java内存区域以及计算机硬件之间的关系,

    • JMM与java内存区域:
      JMM实际上描述的是一种规则,是逻辑上的划分。JMM与Java内存区域唯一相似点,都存在共享数据区域和私有数据区域,在JMM中主内存属于共享数据区域,从某个程度上讲应该包括了堆和方法区,数据线程私有数据区域,从某个程度上讲则应该包括程序计数器、虚拟机栈以及本地方法栈。
    • JMM与计算机内存
      JVM中线程的操作最后都会映射到硬件上,在计算机硬件中,并没有共享数据区域和私有数据区域的划分,实际上都是在计算机内存中。工作内存可能是Cache或者寄存器中,主内存可能直接对应于硬件中的内存。**


      JMM与计算机内存

    相关文章

      网友评论

        本文标题:java并发(二)线程安全性与Java内存模型

        本文链接:https://www.haomeiwen.com/subject/pkhjyftx.html