《深入理解JVM》第十二章 高效并发(JAVA内存模型)

版权声明:版权为ZZQ所有 https://blog.csdn.net/qq_39148187/article/details/81603339

并发处理的广发应用使得Amdahl 定律代替摩尔定律成为了计算机性能发展原动力的根本原因,是人类压榨计算机计算能力的最有利的武器

摩尔定律  : Intel CEO Barret 说没过18 个月芯片的性能就会翻一倍

Amdahl定律:定义了串行系统并行化后的加速比的计算公式和理论上限    1/F+1/n(1-F)   增加CPU处理器的数量并不一定能起到有效的作用
提高系统内可并行化的模块比重,合理增加并行处 理器数量,才能以最小的投入,得到最大的加速比

Gustafson定律(古斯塔夫森)
说明处理器个数,串行比例和加速比之间的关系

只要有足够的并行化,那么加速比和CPU个数成正比

以上说明光靠硬件是不行的,对于我们现在的硬件技术来说, 所以就开始软件方向的考虑

硬件效率一致性

       众所周知,计算机的 内存, 硬盘, cpu 的效率是不在一个等级的,所以绝大数任务都不可能仅仅靠处理器“计算就能完成”处理器也要和内存交互,但是交互过程,内存硬盘,cpu 之间的速度差异,因此现在的计算机都不得不加一层读写速度接近处理器运算速度的缓存(Cacge)作为内存和处理器之间的缓冲,将运算需要的数据复制到缓存中,让运算快速进行,当计算结束后缓存同步内存中,这样就解决了硬件之间的io 的效率的差异性

      虽然这样是解决了硬件io 的速度的差异性,但是他出现了一个新问题(Cache Coherence ) 在多处理器的系统中,每个处理器都有自己的高速缓存,多个处理共享一个内存,如果多个处理器涉及同一个内存共享区,这时候就会导致数据不一致,为了解决这个问题,每个处理器的缓存都要遵循协议, MSI  MESI MOSI  Synapse Firefly Dragon protocol 

       除了加上高速缓存之外,为了使得处理器内存的运算单元得到充分的利用,处理器会对代码进行乱序处理执行优化,处理器会在计算之后将乱序执行的结果重组,保证执行顺序的以执行的结果一致性,但是不能证程序中的每个语句计算的先后顺序和输入代码的顺序一致,因此如果存在一个计算而任务依赖另一个计算任务的中间结果,那么其顺序并不能靠代码的先后顺序保证, java 虚拟机编译的时候也会有指令重排优化,具体优化我会在以后博客上写,可以把java 的class 文件反编译成sam 汇编,然后我们通过汇编命令来分析

java 内存模型

因为C C++ 这些直接操作物理硬件的语言,在不同的硬件上会有不一致性结果,因此java 就想做一个内存模型JMM ,来屏蔽不同硬件操作系统的差异性,定义JMM 不是容易事,这个模型必须定义的够严谨,才能让java 在并发访问内存操作不会气歧义,但是也要定义的宽松,才能使得有足够的空间屏蔽硬件的不一致性,在jdk 5 java 内存模型成熟完善,实现JSR-33

主内存和工作内存

     java 内存模型的主要目标是定义程序中的每个变量的访问规则, 即在虚拟机中将变量储存到内存和从内存去除这样的变量的底层细节,这里变量指的是,实例字段,静态字段,构造成的数组对象,和局部变量,方法参数不是一个意思,因为后者是线程私有的不会共享,因此不存在竞争问题,为了获得较好的执行效率,java 内存模型没有限制执行引擎使用处理器的特定寄存器或者缓存来和内存进行交互,也没有限制即时编译器进行调整代码执行顺序的优化测试

      java 内存模型规定了所有的变量都是存在主存中,这里的主存就是计算机硬件的内存的java虚拟机占有的一部分,每条线程有自己的工作内存,线程的工作内存保存了被该线程使用的主内存的变量的副本,线程对变量的所有操作都必须在工作内存中,不同线程之间的工作内存不可互相操作,他们只能通过主存然后进行交互,此处讲的内存模型, 和java 的堆栈方法区内存模型讲的不是一个东西,是从不同层次上的划分,有个注释

工作内存复制的是主内存中的引用,如果该类型是一个引用类型的话

内存之间的交互操作

java 内存模型中定义了8 中操作来完成,虚拟机实现时必须要保证下面提及的每一种操作都是原子性的。不可再分的。对于double 和long类型的变量来说,load store read write 操作在某些平台上有例外,下文会讲到

lock(锁定):作用于主内存的变量,他把一个变量标识位一个线程独占的状态

unlock(解锁):作用于主内存的变量,他把一个处于锁定状态的变量释放出来,释放后的变量可以被其他线程锁定

read(读取): 作用主内存的变量,把一个变量的值从主内存中传输到线程的工作内存中一边随后执行load动作使用

load(载入):作用于工作内存的变量,他把read 操作从主内存得到的变量放入工作内存的变量副本中

use(使用) : 作用于工作内存的变量,他把工作内存中的一个变量值传给执行引擎,每当虚拟机遇到一个要使用的变量的值的字节指令时就会执行这个命令

assign (赋值):作用于工作内存的变量,把一个执行引擎接受的值复制给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码执行时执行这个操作

store (存储):用于工作内存,他把一个工作内存中的值,传给主内存中,一边随后 write 操作使用

wtite(写入):作用于主内存的变量,他把sore 操作从工作内存中得到的变量放入主内存中

如果要从主内存复制副本到工作内存中就要按顺序执行read load操作

如果要把工作内存中的数据同步会主内存就要执行store 和write 操作

java 内存模型只要求上述操作必须是按顺序执行的没有保证是连续性的执行,页就是说read load 之间可以插入其他的操作,

   1.不允许read load   和 store write 操作之一单独的出现,

    2.不允许一个线程丢弃最近的一个assign 操作,即变量在工作内存中受线程改变之后,必须要同步到主内存中

    3.一个变量的诞生只能发生在主内存中,也就是一个变量在操作use store 之前,必须经过assign 和load 操作

    4.一个变量在同一时刻只允许一条线程进行lock 操作,但是lock 操作可以被多个线程重复多次使用,多次使用lock 之后只有执行想通过的unlock 操作变量才会被解锁

      5. 如果一个变量没有执行过lock 操作,不允许执行unlock 操作

volatile型变量的特殊规则

   被volatile 修饰之后,此变量具备两个特征

1.此变量具有可见性,

可见性相对应的是内存模型中的不可见性,也就是在变量在工作内存中被改变之后要通过store write操作把变量写到主内存中其他线程才知道这个变量被改变了,这里是可见性说的是不用同步到主内存这个变量的改变,其他线程就知道,但是这个过程需要一个刷新,所以volatile 变量是存在不一致性的,java使用volatile 变量进行运算不具有原子性,所以volatile 变量的运算在并发下是不安全的,我们代码验证

package com.jvm.volatiletest;

import java.util.concurrent.CountDownLatch;

/**
 * @author ZZQ
 * @date 2018/8/13 20:00
 * 测试volatile 计算的非原子性
 */
public class VolatileTest {


    public static volatile  int race =0 ;

    public static void increase(){
        race++;
    }

    private static final  int THREADS_COUNT=20 ;

    public static void main(String[] args)  throws  Exception{
        CountDownLatch latch = new CountDownLatch(THREADS_COUNT); // 同步屏障
        Thread[] threads = new Thread[THREADS_COUNT];
        for(int i = 0  ;i<THREADS_COUNT ;i++){
            threads[i] = new Thread(()->{
                for(int j = 0 ; j <1000 ; j++){
                    increase();
                }
                latch.countDown(); // 计算完一个减少一个屏障标识
            });
            threads[i].start();
        }


        latch.await();//阻塞线程只要屏障标识为0
        System.out.println(race);

    }

}

这里的结果每次都是小于200000的说明了,volatile 变量的计算不是安全的, 我们从字节码指令角度看, 使用javap -c 把class 文件反编译成jvm 字节码指令 

Compiled from "VolatileTest.java"
public class com.jvm.volatiletest.VolatileTest {
  public static volatile int race;

  public com.jvm.volatiletest.VolatileTest();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void increase();
    Code:
       0: getstatic     #2                  // Field race:I
       3: iconst_1
       4: iadd
       5: putstatic     #2                  // Field race:I
       8: return

  public static void main(java.lang.String[]) throws java.lang.Exception;
    Code:
       0: new           #3                  // class java/util/concurrent/CountDownLatch
       3: dup
       4: bipush        20
       6: invokespecial #5                  // Method java/util/concurrent/CountDownLatch."<init>":(I)V
       9: astore_1
      10: bipush        20
      12: anewarray     #6                  // class java/lang/Thread
      15: astore_2
      16: iconst_0
      17: istore_3
      18: iload_3
      19: bipush        20
      21: if_icmpge     52
      24: aload_2
      25: iload_3
      26: new           #6                  // class java/lang/Thread
      29: dup
      30: aload_1
      31: invokedynamic #7,  0              // InvokeDynamic #0:run:(Ljava/util/concurrent/CountDownLatch;)Ljava/lang/Runnable;

      36: invokespecial #8                  // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
      39: aastore
      40: aload_2
      41: iload_3
      42: aaload
      43: invokevirtual #9                  // Method java/lang/Thread.start:()V
      46: iinc          3, 1
      49: goto          18
      52: aload_1
      53: invokevirtual #10                 // Method java/util/concurrent/CountDownLatch.await:()V
      56: getstatic     #11                 // Field java/lang/System.out:Ljava/io/PrintStream;
      59: getstatic     #2                  // Field race:I
      62: invokevirtual #12                 // Method java/io/PrintStream.println:(I)V
      65: return

  static {};
    Code:
       0: iconst_0
       1: putstatic     #2                  // Field race:I
       4: return
}

从上看出 ,一个increase 方法被编译成了 4 行指令, retrun不算, 这样来说,在指令执行的过程中,有没有可能别的线程执行指令呢? 

从字节码层次已经找到线程不安全的问题

     由于volatile 变量只能保证可见性,不能保证原子性,因此我们要加锁,使用synchronized 或者java.util.concurrent 中的原子类保证原子性个,

    1 . 运算结果并不依赖变量的当前值,或者能够确保只有一个单线程修改变量的值

    2 . 变量不需要与其他的状态变量共同参与不变约束

2. 使用volatile 变量的第二个语义 是禁止指令重排序优化

普通的变量只能保证取出变量和写入变量值的正确性,但是不能保证程序代码中执行的顺序一致性,在一个线程方法中不能感到这一点,这就是java 内存模型中描述的所谓的“线程比表现的串行化语义”

其实这个意思就是,如果没有volatile ,线程和线程之间是不可见的, 因此,如果并发同时修改变量就会出现线程并发问题,如果加上volatile 关键字,线程处理结果之后会快速刷新同步到主内存实现内存的可见性,然后这样线程和线程之间就会有可见性,因此,jvm 不会对指令进行重排,(volatile 是在jdk 1.5才完全屏障指令重排)

我们发现被volatile修饰的变量复制后多了lock add1 $0x0,(%esp) 操作,这个操作相当于一个屏障指重排序时不能把后面的指令重排到内存屏障之气那的未知,只有cpu访问内存时,并不需要内存平衡轴那个,但如果是两个CPU同时访问一个内存,且其中有个观测另一个,哪就需要内存屏障来保证一致性,关键在于lock 前缀,他的作用使cpu的cache写入了内存,对于cache 中的变量做了一个前面介绍的java内存模型中所说的store 和write 操作,所以通过这个空操作, 可让前面volatile变量修改让其他cpu看到

    volatile 比其他的同步代码工具更快? volatile 的同步机制的性能要高于锁synchronized 和concurrent 里面的锁,但是由于虚拟机中对所实施了很多的消除优化,使得我们很难量化的认为volatile 就会比synchnronized 快多少,

对于long double 类型变狼的特殊规则

java 内存模型lock unlock read load  assign use store write 操作都有原子性,但是对于64 位的数据定义了宽松的规定,允许虚拟机将灭有被volatile 修饰的64位数据的独写操作分为两次32 操作,这就是long double 的非原子性协议,如果多线程共享一个未声明的volatile 的long和double 类型的变量,同时进行修改操作, 那么某些线程可能会读到一个“半个变量”这种情况罕见 在商用jvm不会出现,java 内存模型虽然允许虚拟机不把long和double 变量的独写实现成原子操作,但是虚拟机选择性把把这些操作实现了原子性,因此开发中long double 类型的变量不必写volatile

原子性 / 可见性/ 有序性  

     原子性   由java内存模型直接保证的原子性变量操作包括read load assign use  store write ,我们大致可以认为基本数据类型的冯文独写都具有原子性,但是long double 的非原子性协议,就不一定 , 尽管虚拟机未把lock unlock 操作直接开发给用户使用,但是提供了两个字节码指令,monitoretrer 和monitorexit 来隐式的使用这两个操作,这两个字节码映射到java 代码中就式synchronized关键字,因此在synchronized 之间的操作就具有原子性

    可见性   可见性是指一个线程修改了共享变量,其他线程能够立即得知,volatile 的特殊保证了新值能立即同步到主内存中,每次使用前立即从主内存中刷新,因此可以说volatile 保证了多线程的变量可见性, 除此之外,java 中还有两个关键字可以保证可见性,final 和synchronized ,同步快的可见性是由对一个变量执行unlock 操作之前,必须把此变量同步到主内存中,(执行store write 操作) final 是指被final修饰的字段在构造器中以一旦初始化完成并且构造器没有把this 的引用传递出去,哪在其他线程中就看到final字段的值,

    有序性  java 内存模型有序性volatile 讨论过,java 程序中的天然有序性可以总结为一句话,如果在本线程中观察,所有的操作都是有序的,如果在本线程观察另一个线程所有的操作都是无序的,前半句为线程内表现为串行化语义,后半句指,指令重排,公安做内存和主内存同步延迟,java提供volatile synchronized 保证线程的有序性,volatile 关键字禁止了指令重排,synchronized 通过lock unlock 

现行并发原则

其实这些原则写代码的时候都知道。我就直接复制粘贴


 

猜你喜欢

转载自blog.csdn.net/qq_39148187/article/details/81603339