JMM ---- Java内存模型

131 阅读6分钟

Java内存模型

JVM内存结构和Java虚拟机的运行时区域有关

Java内存模型和Java并发有关

Java对象模型,Java对象在虚拟机中的表现形式,Java对象本身的存储模型

JMM(Java Memory Model)

  1. 是一种规范,为了让多线程运行结果可预期
  2. JMM是工具类和关键字的原理
    • volatile, synchronized, lock的原理都是JMM
  3. 最重要的三个内容:重排序,可见性,原子性

为什么需要JMM:

例如C语言没有内存模型,依赖处理器的运行,不同的处理器结果不同,也就无法保证并发安全 对于Java来说,虚拟机不只一种,也就更需要一种统一的内存模型来规范了

重排序

什么是重排序

public class OutOrderExecution {
    private static int a = 0, b = 0;
    private static int x = 0, y = 0;

    public static void main(String[] args) throws InterruptedException {

        for (int i = 0; ; i++) {
            x = 0;
            y = 0;
            a = 0;
            b = 0;
            //CountDownLatch latch = new CountDownLatch(1); // 栅栏,能够让两个线程等待,然后同时执行
            Thread one = new Thread(new Runnable() {
                @Override
                public void run() {
                    /*try {
                        latch.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }*/
                    a = 1;
                    x = b;
                }
            });
            Thread two = new Thread(new Runnable() {
                @Override
                public void run() {
                    /*try {
                        latch.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }*/
                    b = 1;
                    y = a;
                }
            });
            one.start();
            two.start();
            //latch.countDown();// 放开
            one.join();
            two.join();

            String res = "执行第" + i + "次" + "(x = " + x + ", y = " + y + ")";
            if (x == 0 && y == 0) {
                System.out.println(res);
                break;
            }else if (x == 1 && y == 1) {
                System.out.println(res);
                break;
            } else {
                System.out.println(res);
            }
        }

    }
}

运行结果会出现四种情况 01, 10, 11,00

其中00这种情况的出现的原因: 在线程内部代码执行的顺序和实际Java程序编写代码的顺序不一样,也就是代码指令并不是严格按照代码语句顺序执行的,执行顺序被改变了,这种现象就是重排序

重排序的好处:提高处理速度,因为重排序会对指令进行优化

重排序的三种情况(这个我还不懂):1. 编译器优化 2. CPU指令重排,3. 内存的“内存的重排序”

可见性

public class FiledVisibility {
    int a = 1;
    volatile int b = 2;
    private void change() {
        a = 3;
        b = a; // volatile能保证能保证读到的一定是3,也就是可以保证可见性
    }
    private void print() {
        System.out.println("a = " + a + ", b = " + b);
    }

    public static void main(String[] args) {
        while (true) {
            FiledVisibility test = new FiledVisibility();
            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();
        }
    }
}

1. 什么是可见性

可见性就是指当一个线程修改了共享变量的值时, 其他线程能够立即得知这个修改

JMM的抽象了主内存和本地内存

JMM为了提高读取的效率,定义了一套读写内存的规范,不需要关心一级缓存和二级缓存(CPU的处理速度远快于从内存中读取的读取速度,因此CPU和内存之间临界存储区域也就是缓存)的问题,JMM抽象出主内存和本地内存的概念

2. 主内存和本地内存的关系

  1. 所有变量都存储在主内存中,同时每个线程都有自己的工作内存,工作内存中的变量内容是主内存的拷贝

  2. 线程不能直接读取主内存中的变量,只能操作自己工作内存中的变量,然后同步到主内存中

  3. 主内存是多个线程共享的,但线程间不共享内存,如果线程间需要通信,必须通过主内存中转完成

总结:所有的共享变量都存在主内存中,每个线程有自己的本地内存,而对线程读写共享数据也是通过本地内存交换,(交换的过程不是实时的)所以才导致可见性问题

3. Happens-Brfore

用来解决可见性问题的,在时间上,动作A发生在动作B之前,B保证能看见A

另一种解释:如果一个操作happens-before于另一个操作,那么我们说第一个操作对于第二个操作是可见的

什么不是Happens-Brfore

两个线程没有相互配合的机制,所以代码X和Y的执行结果并不能够保证总被对方看到的,这就不具备Happens-Brfore

Happens-Brfore原则的应用

  1. 单线程原则
  2. 锁操作
  3. volatile 变量
  4. 线程启动
  5. 线程join
  6. 传递性
  7. 中断
  8. 构造方法
  9. 工具类的Happens-Brfore原则 ConcurrentHaspMap, get()一定能看到之前put()的操作

4.volatile关键字

volatile是什么 是一种同步机制,比synchronize或Lock相关类更加轻量,因为volatile并不会发生上下文切换等开销很大的行为。如果一个变量被修饰成volatile,那么JVM就知道这个变量可能会被并发修改。

开销小,相应的能力相对也小,虽说是用来同步保证线程安全的,但是做不到像synchronize那样的原子保护,所以使用场景有限

volatile的适用场合

  1. 运行不取决于之前的状态
  2. 刷新之前的触发器
    int a = 1;
    int c = 20;
    volatile int b = 2;
    private void change() {
        a = 3;
        c = 40;
        b = 0; // volatile能保证能保证读到的一定是0,而且能保证a。c的值修改成之后的最新的值
    }
    private void print() {
        if(b == 0) {
            System.out.println("a = " + a + ", b = " + b + ", c = " + c);
        }
    }

volatile的作用:保证可见性、禁止重排序

  1. 可见性:读取一个volatile变量之前,需要先使相应的本地缓存失效,这样就必须到主内存中读取最新值,写一个volatile属性会立即刷入到主内存

  2. 禁止指令重排序的优化:解决单例模式双重锁乱序的问题

volatile和synchronize的关系:

volatile可以被看作成轻量版的synchronize:如果一个共享变量至始至终都只是被各种线程赋值,而没有其他操作,那么可以用volatile可以来代替synchronize或代替原子变量,因为赋值自身是有原子性的,而volatile又保证了可见性,所以就足以保证线程安全

用volatile可以修正重排序的问题

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

5.能保证可见性的措施

synchronize不仅保证了原子性也保证了可见性 synchronize不仅让被保护的代码安全了也让被保护代码之前的代码可见类似volatile触发器的作用

原子性

什么是原子性:

一系列的操作要么全部执行成功要么全部不执行,不会出现执行一半的情况,是不可分割的 例如a++就不是原子性的

Java中具有原子性的操作(原子操作) 除了long、double之外基本类型的赋值类型 所有引用的赋值操作

java.concurrent.Atomic.*包中所有类的原子操作

long double在32位虚拟机上不是原子的,分成两个32位进行读写,可以使用volatile解决 在64位JVM上是原子的,实际开发中虚拟机考虑带这些影响,我们就不需要关心long double出现的问题

原子操作 + 原子操作 != 原子操作