1. 首页
  2. Java

java并发编程底层原理-java内存模型

一、JMM(Java Memory model)是什么

1.从java代码到cpu指令
  • 最开始,我们编写的ava代码,是*java文件
  • 在编译( Javac命令)后,从刚才的* java文件会变出一个新的ava字节码文件(*class)
  • JVM会执行刚才生成的字节码文件(*class),并把字节码文件转化为机器指令
  • 机器指令可以直接在CPU上执运行,也就是最终的程序执行
      不同的JVM实现会带来不同的”翻译”,不同的CPU平台的机器指令又千差万别;所以我们在Java代码层写的各种Lock,其实最后依赖的是JVM的具体实现(不同版本会有不同实现)和CPU的指令,才能帮我们达到线程安全的效果。
       由于最终效果依赖处理器,不同处理器结果不一样,这样无法保证并发安全,所以需要一个标准,让多线程运行的结果可预期,这个标准就是JMM。
2.为什么需要JMM

需要一个标准,让多线程运行的结果可预期,JMM实际上是一组规范,需要各个JVM的实现来遵守JMM规范,以便于开发者可以利用这些规范,更方便地开发多线程程序。
如果没有JMM,则需要自己指定什么时候用内存栅栏等,有了JMM只需要用同步工具和关键字就可以开发并发程序

二、JMM的主要内容

最重要的3点内容:重排序、可见性、原子性

1.重排序
  • 1.1什么是重排序
    代码的实际执行顺序和代码在Java文件中的顺序不—致,代码指令并不是严格按照代码语句顺序执行的,它们的顺序被改变了,这就是重排序。
public class OutOfOrderExecution {

    private static int x = 0, y = 0;
    private static int a = 0, b = 0;

    public static void main(String[] args) throws InterruptedException {
        int i = 0;
        for (; ; ) {
            i++;
            x = 0;
            y = 0;
            a = 0;
            b = 0;

            CountDownLatch latch = new CountDownLatch(3);

            Thread one = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        latch.countDown();
                        latch.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    a = 1;
                    x = b;
                }
            });
            Thread two = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        latch.countDown();
                        latch.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    b = 1;
                    y = a;
                }
            });
            two.start();
            one.start();
            latch.countDown();
            one.join();
            two.join();

            String result = "第" + i + "次(" + x + "," + y + ")";
            if (x == 0 && y == 0) { //发生重排序
                break;
            }
        }
    }
}
  • 1.2重排序的好处
    提高处理速度
  • 1.3重排序的3种情况
    • 编译器优化:包括JVM、JIT编译器等
      编译器(包括JVM、JIT编译器等)出于优化的目的(例如当前有了数据a,那么如果把对a的操作放到一起效率会更高,避兔了读取b后又返回来重新读取a的时间开销),在编译的过程中会进行一定程度的重排,导致生成的机器指令和之前的字节码的顺序不一致。
    • CPU指令重排:CPU也可能对指令进行重排
      CPU的优化行为,和编译器优化很类似,是通过乱序执行的技术,来提高执行效率。所以就算编译器不发生重排,CPU也可能对指令进行重排,所以我们开发中,一定要考虑到重排序带来的后果。
    • 内存的”重排序”:线程A的修改线程B却看不到(可见性问题)
      内存系统内不存在重排序,但是内存会带来看上去和重排序一样的效果。由于内存有缓存的存在,在JMM里表现为主存和本地内存,由于主存和本地内存的不一致,会使得程序表现出乱序的行为。
2.可见性
  • 2.1什么是可见性
public class FieldVisibility {

     int a = 1;
     int b = 2;

    private void change() {
        a = 3;
        b = a;
    }

    private void print() {
        System.out.println("b=" + b + ";a=" + a);
    }

    public static void main(String[] args) {
        while (true) {
            FieldVisibility test = new FieldVisibility();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    test.change();
                }
            }).start();

            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    test.print();
                }
            }).start();
        }

    }![](http://www.caobinrg.com/wp-content/uploads/2020/04/2020041915090564.png)
}
    输出结果为4中情况:
    a=3,b=2
    a=3,b=3
    a=1,b=2
    a=1,b=3(出现可见性问题)
  • 2.2 为什么会出现可见性问题
    CPU有多级缓存,导致读的数据过期

    • 高速缓存的容量比主内存小,但是速度仅次于寄存器,所以在CPU和主内存之间就多了Cahe层
    • 线程间的对于共享变量的可见性问题不是直接由多核引起的,而是由多缓存引起的。
    • 如果所有个核心都只用一个缓存,那么也就不存在内存可见性问题
    • 每个核心都会将自己需要的数据读到独占缓存中,数据修改后也是写入到缓存中,然后等待刷入到主存中。所以会导致有些核心读取的值是一个过期的值。
      java并发编程底层原理-java内存模型
  • 2.3 JMM的抽象:主内存和本地内存
    Java作为高级语言,屏蔽了内存的底层细节,而用JMM定义了一套读写内存数据的规范,抽象出了主内存和本地内存的概念,我们不再需要关心一级缓存和二级缓存的问题。
    本地内存并不是真的是一块给每个线程分配的内存,而是对于寄存器、一级缓存、二级缓存等的抽象。
    java并发编程底层原理-java内存模型

    JMM对于本地内存和主内存做了一下规定:

    • 所有的变量都存储在主内存中,同时每个线程也有自己独立的工作内存,工作内存中的变量内容是主内存中的拷贝
    • 线程不能直接读写主內存中的变量而是只能操作自己工作內存中的变量,然后再同步到主内存中
    • 主内存是多个线程共享的,但线程间不共享工作內存,如果线程间需要通信,必须借助主内存中转来完成

    所有的共享变量存在于主内存中,每个线程有自己的本地内存,而且线程读写共享数据也是通过本地内存交换的,所以才导致了可见性问题。

  • 2.4 Happens-Before原则

    happens-before规则是用来解决可见性问题的:在时间上,动作A发生在动作B之前,B保证能看见A,这就是happens-before。
    两个线程没有相互配合的机制,代码1和2的执行结果并不能保证总被对方看到的,这就不具备happens-before。

    Happens-Before的规则:

    • 单线程规则:单线程内后面的语句一定能看到前面语句的动作,Happens-Before不会影响重排序
    • 锁操作(synchronized和Lock)
      java并发编程底层原理-java内存模型
    • volatile变量
    • 线程启动
    • 线程join:后续语句不会进行重排序
    • 传递性
    • 中断:一个线程被其他线程Interrupt时,那么检测中断(inTerrupted)或者抛出 interruptedException一定能看到。
    • 构造方法
    • 工具类的Happens-Before原则
    • 线程安全的容器get一定能看到在此之前的put等存入动作
    • CountDownLatch
    • Semaphore
    • Future
    • 线程池
    • CyclicBarrier
  • 2.5 volatile详述
    volatile是一种同步机制,比synchronized或者LoCk相关类更轻量,因为使用volatile并不会发生上下文切换等开销很大的行为。
    如果一个变量别修饰成 volatile,那么JVM就知道了这个变量可能会被并发修改
    开销小,相应的能力也小,虽然说volatile是用来同步的保证线程安全的,但是volatile做不到synchronized那样的原子保护,volatile仅在很有限的场景下才能发挥作用。

    • volatile的作用:
      • 可见性:读一个 volatile变量之前,需要先使相应的本地缓存失效,这样就必须到主内存读取最新值,写一个 olatile属性会立即刷入到主内存。
      • 禁止指令重排序优化:解决单例双重锁乱序问题
    • volatile使用范围:
      • 适用场合1:boolean flag,如果一个共享变量自始至终只被各个线程赋值,而没有其他的操作,那么就可以用 volatile来代替synchronized或者代替原子变量,因为赋值自身是有原子性的,而volatile保证了可见性,所以就足以保证线程安全。(如果需要重新取值再赋值的操作同样不适用)
        private void setFlag() { //适用
        flag = true;
        }
        private void setFlag() { //不适用
        flag = !flag;
        }
        
      • 适用场合2:作为刷新之前变量的触发器
    • volatile和synchronized的关系:
        volatile可以看做是轻量版的synchronized:如果一个共享变量自始至终只被各个线程赋值,而没有其他的操作,那么就可以用volatile来代替 synchronized或者代替原子变量,因为赋值自身是有原子性的,而 volatile又保证了可见性,所以就足以保证线程安全。
        volatile属性的读写操作都是无锁的,它不能替代synchronized因为它没有提供原子性和互斥性。因为无锁,不需要花费时间在获取锁和释放锁上,所以说它是低成本的。
3.原子性
  • 3.1 什么是原子性
    一系列的操作,要么全部执行成功,要么全部不执行,不会出现执行一半的情况,是不可分割的。
  • 3.2 java中的原子操作
    除long和double之外的基本类型(int,byte, boolean, short,char float)的赋值操作
    所有引用 reference的赋值操作,不管是32位的机器还是64位的机器
    java. concurrent Atomic.*包中所有类的原子操作
  • 3.3 long和double原子性问题
    出于Java编程语言存储器模型的目的,对非易失性long或double值的单个写入被视为两个单独的写入:每个32位半写一个。这可能导致线程从一次写入看到64位值的前32位,而从另一次写入看到第二次32位的情况。
    鼓励java虚拟机的实现避免在可能的情况下拆分64位值。鼓励程序员将共享的64位值声明为vo1atie或正确同步其程序以避免可能的复杂情况

原创文章,作者:小童子,如若转载,请注明出处:https://www.caobinrg.com/506.html

发表评论

电子邮件地址不会被公开。 必填项已用*标注

联系我们

邮件:caobinrg@163.com