java内存模型JMM-底层原理

java内存模型JMM-底层原理

底层原理

重点:懂了这个,才能说懂了并发,才能说懂了java

java到CPU

image-20210103212244110

JVM实现会带来不同的“翻译”,不同的CPU平台的机器指令又千差万别,无法保证并发安全效果一致。所以转换过程需要有规范和原则.

JVM内存结构 Java内存模型 Java对象模型

JVM内存结构

和java虚拟机的运行时区域有关

image-20210103213617892

堆:最大的一块,new 或其他创建的实例对象,数组. 优势就是动态分配

虚拟机栈:java栈.保存了各个基本的数据类型,以及对象的引用 编译的时候确定了大小,且编译的时候不会改变

方法区:static变量,类信息,常量信息,永久引用(static).

本地方法栈:和本地方法相关的,native方法

程序计数器:同计组.

Java内存模型

和java并发编程相关

java对象模型

和Java对象在虚拟机中的表现形式有关

image-20210103214230697

  • java对象自身的存储模型
  • JVM会给这个类创建一个instanceKlass,保存在方法区,用来在JVM层表示该java类
  • 当我们在Java代码中,使用new创建一个对象的时候,JVM会创建一个instanceOopDesc对象,这个对象中包含了对象头以及实例数据.

JMM Java Memory Model

  • C不存在内存模型的概念.
  • 依赖处理器,不同处理器结果不一样.
  • 无法保证并发安全.
  • 需要一个标准,让多线程运行的结果可预期.

JMM是规范

如果没有这个,经过不同JVM的不同规则的重排序 之后,会导致不同的虚拟机上运行的结果不一样,这就缺失了跨平台性.

是原理

是volatile,synchronized,lock的原理都是JMM,如果没有JMM,那就需要我们自己指定什么时候用内存栅栏等…

重排序,可见性,原子性.

重排序

package com.hasson.jmm;

import java.util.concurrent.CountDownLatch;

//演示重排序,知道达到某个条件才停止,测试小概率事件
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 = y = a = 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();
            System.out.println("执行了" + i + "次");
            System.out.println("x = " + x + " y = " + y);
            if (x == 1 && y == 1) {
                break;
            }
        }
    }
}

但是经过重排序之后,仍有可能出现x=0,y=0的情况.即以下顺序image-20210107135910109

y=a;
a=1;
x=b;
b=1;

什么是重排序:

在线程1内部的两行代码的实际执行顺序和代码在Java文件中的顺序不一致,代码指令不不是严格按照代码语句顺序执行的,他们的顺序被改变了,这就是重排序,这里被颠倒的是y=1,b=1这两行语句.

a=1 会被拆为: Load a,set to 3,Store a

好处是提高处理速度.

image-20210107194452930

三种情况

  • 编译器优化:包括JV吗,JIT编译器
  • CPU指令重排:就算编译器不发生重排,CPU也可能重拍.
  • 内存的"重排序":线程A的修改线程B却看不到,引出可见性问题

可见性

什么是可见性问题

  1. image-20210107201410418

  2. package com.hasson.jmm;
    
    //演示可见性带来的问题
    public class FieldVisibility {
        int a = 1;
        int b = 2;
    
        public static void main(String[] args) {
            while (true) {
                FieldVisibility fieldVisibility = new FieldVisibility();
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            Thread.sleep(1);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        fieldVisibility.change();
                    }
                }).start();
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            Thread.sleep(1);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        fieldVisibility.print();
                    }
                }).start();
                // a =3 b =3
                // a= 3 b =2
                // a= 2 b =1
                // a= 1 b =3
            }
        }
    
        private void print() {
            System.out.println("a = " + a + " b = " + b);
        }
    
        private void change() {
            a = 3;
            b = a;
        }
    }
    
  3. 线程b看不到(或看不完全)线程A的操作.

  4. image-20210107201759643
  5. 线程不能直接通信,需要通过shared cache

  6. 用volatile,可以立马刷新.

为什么会有可见性问题

  1. image-20210107203258730
  2. 由于CPU有多级缓存,会导致读的数据过期

JMM抽象:主内存和本地内存

Java作为高级语言,屏蔽了这些底层细节,用JMM定义了读写内存数据的规范,虽然我们不再需要关心一级缓存和二级缓存的问题,但是JMM抽奖了主内存和本地内存的概念.

这里说的本地内存并不是一块给每个线程分配的内存,而是JMM的一个抽象,是对于寄存器,一级缓存,二级缓存等的抽象.

image-20210107205149433

JMM规定:

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

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

Happens-Before原则

单线程规则

image-20210107210937211一个线程里:后面的语句一定发生在前面的语句之后,肯定看得到前面执行了什么

锁操作(synchronized和lock)

image-20210107211203168如x =1,i=x

volatile变量

image-20210107211342595

加了volatile,就可以有happen-before

线程启动

image-20210107211455107

线程join

image-20210107211601067

join就是想等待。

传递性

如果hb(a,b) and hb(b,c) so hb(a,c)

中 断

一个线程被其他线程interrupt时,那么检测中断(isInterrupted)或者抛出InterruptedException一定能看到,得要检测到。

构造方法

对象构造方法的最后一行指令happeds-before于finalize方法的第一行指令

线程工具类中

  1. 线程安全的容器get一定能看到在此之前的put等存入动作
  2. CountDownLatch
  3. Semaphore
  4. Future
  5. 线程池
  6. CyclicBarrier

volatile关键字

含义

volatile是一种同步机制,比synchronized或者Lock相关类更轻量,因为使用volatile并不会发生上下文切换等开销很大的行为。

如果一个变量被修饰为volatile,那么JVM就知道这个变量可能被并发修改。

但是开销小,相应的能力也小,虽然说volatile是用来同步的保证线程安全的,但是volatile做不到synchronized那样的源自保护,volatile尽在很有限的场景瞎才能发挥作用。

适合和不适合的场景

不适用:a++

使用场合:

适用场合1: boolean flag,如果个共享变量自始至终只被各个线程赋值,而没有其他的操作(对比,取值等等),那么就可以用 volatile来代替synchronized或者代替原子变量,因为赋值自身是有原子性的,而volatile又保证了可见性,所以就足以保证线程安全。而不取决于先前的情况

适用场合2:作为刷新之前变量的触发器

作用

  1. 可见性:读个 volatile変量之前,需要先使相应的本地缓存失效,这样就必须到主内存读取最新值,写一个 volatile属性会立即刷入到主内存。
  2. 禁止指令重排序优化:解決单例双重锁乱序问题

volatile和synchronized的关系

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

小结

  1. volatile修饰符适用于以下场景:某个属性被多个线程共享,其中有一个线程修改了此属性,其他线程可以立即得到修改后的值,比如boolean flag;或者作为触发器,实现轻量级同步。

  2. volatile 属性的读写操作都是无锁的,它不能替代 synchronized因为它没有提供原子性和互斥性。因为无锁,不需要花费时间在获取锁和释放锁上,所以说它是低成本的。

  3. volatile只能作用于属性,我们用 volatile修饰属性,这样compilers就不会对这个属性做指令重排序。

  4. volatile提供了可见性,任何一个线程对其的修改将立马对其他线程可见。 volatile属性不会被线程绶存,始终从主存中读取

  5. volatile提供了 happens- before保证,对 volatile变量v的写入 happens- before所有其他线程后续对y的读操作。

  6. volatile 可以使得long和 double I的赋值是原子的,后面马上会和 double的原子性。

保证可见性的措施

除了 volatile可以让变量保证可见性外, synchronizedLock并发集合、 Thread.join()和 **Thread. start()**等都可以保证的可见性

也就是happen-before

synchronized可见性

synchronized不仅保证了原子性,还保证了可见性

上一个synchronized可以被下一个看到,附近的也会,如下图:image-20210109164514898

如print函数可以看到change函数的所有改变。

原子性

什么是原子性

系列的操作,要么全部执行成功,要么全部不执行,不会出现执行一半的情况,是不可分割的。–ATM取钱

Java中的原子操作有哪些

  • 除了long double之外的基本类型(int,byte,boolean,short,char,float)的赋值操作。
  • 所有引用reference的赋值操作,不管是32位还是64位的机器。
  • java.concurrent.Atomic.*包中所有类的原子操作

long和double 的原子性

问题描述:官方文档、对于64位的值的写入,可以分为两个32位的操作进行写入、读取错误、使用 volatile解决。

32位上的JVM不是原子的,64位的JVM是原子的。

在实际上不会出现.

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

面试问题

单例模式

  • 节省内存和计算
  • 保证结果正确
  • 方便管理

适用场景

  1. 无状态的工具类:比如日志工具类,不管是在哪里使用,我们需要的只是它帮我们记录日志信息,除此之外,并不需要在它的实例对象上存储任何状态,这时候我们就只需要一个实例对象即可。

  2. 全局信息类:比如我们在一个类上记录网站的访问次数,我们门不希望有的访问被记录在对象A上,有的却记录在对象B上,这时候我们就让这个类成为单例。

各种写法.

饿汉式(静态变量)(可用)

//静态常量(饿汉式)(可用)
public class Singleton1 {
    private final static Singleton1 INSTANCE = new Singleton1();

    private Singleton1() {
    }

    public static Singleton1 getInstance() {
        return INSTANCE;
    }
    //简单
    //加载类的时候就创建好了。
}

饿汉式(静态代码块)(可用)

//静态代码块(饿汉式)(可用)
public class Singleton2 {
    private final static Singleton2 INSTANCE;

    static {
        INSTANCE = new Singleton2();
    }

    private Singleton2() {
    }

    public static Singleton2 getInstance() {
        return INSTANCE;
    }
    //和第一种类似。
}

懒汉式(线程不安全)不可用

//懒汉式(线程不安全)不可用
public class Singleton3 {
    private static Singleton3 instance;

    private Singleton3() {

    }

    public static Singleton3 getInstance() {
        if (instance == null)  //这里切换线程的时候,会生成两个,就不是同一个实例了。
            instance = new Singleton3();
        return instance;
    }
}

懒汉式(线程安全,用同步)[不推荐使用]

//懒汉式(线程安全)不推荐用,效率太低了~
public class Singleton4 {
    private static Singleton4 instance;
    private Singleton4() {
    }
    public synchronized static Singleton4 getInstance() {
        if (instance == null)  
            instance = new Singleton4();
        return instance;
    }
}

懒汉式(线程不安全,同步代码块)[不可用]

//懒汉式(线程不安全,同步代码块)[不可用]
public class Singleton5 {
    private static Singleton5 instance;

    private Singleton5() {
    }

    public static Singleton5 getInstance() {
        if (instance == null)  
            synchronized (Singleton5.class) { //会生成俩
                instance = new Singleton5();
            }
        return instance;
    }
}

双重检查[推荐使用] 面试的时候使用

//双重检查[推荐使用]
public class Singleton6 {
    private volatile static Singleton6 instance;

    private Singleton6() {
    }

    public static Singleton6 getInstance() {
        if (instance == null)
            synchronized (Singleton6.class) {
                if (instance == null)  //双重检查~~
                    instance = new Singleton6();
            }
        return instance;
    }
}

好处:

  • 线程安全,延迟加载,效率比较高

为什么要double-check

  • 为了线程安全,单check的时候不安全,可以举上面的例子(同时进入第一个判断).

synchronized放到方法上可以嘛?

  • 可以的,只是性能不太好.

为什么要用volatile?

  1. 新建对象有三个步骤(不是原子操作)image-20210109192412853可能还是会发生重排序
  2. 重排序会导致空指针问题,所以要防止重排序
  3. 所以使用volatile

静态内部类[推荐使用]

//静态内部类[推荐使用]--属于懒汉,区别于第一、二种形式,他们有时候不需要这个实例,也创建了。
//但是静态内部类这种方法只有需要的时候才会创建。(反射的知识。)
public class Singleton7 {

    private Singleton7() {
    }

    private static class SingletonInstance {
        private static final Singleton7 INSANCE = new Singleton7();
    }

    public static Singleton7 getInstance() {
        return SingletonInstance.INSANCE;
    }
}

枚举[推荐使用]

//最佳的方法  枚举形式
//调用方法 Singleton8.INSTANCE
public enum Singleton8 {
    INSTANCE;

    public void func() {

    }
}
不同写法的对比
  • 饿汉:简单,但是没有lazy loading(需要才加载)
  • 懒汉:线程不安全
  • 静态内部类:可用
  • 双重检查:可考察的知识点比较多.
  • 枚举:最好
    • Joshua Bloch大神在《 Effective Java》中明确表达过的观点:“使用枚举实现单例的方法虽然还没有广泛采用,但是单元素的枚举类型已经成为实现 Singleton的最佳方法。
    • 写法简单
    • 线程安全有保障–里面的知识比较多
    • 避免反序列化破坏单例
不同方法的适用场合

最好的方法是利用枚举,因为还可以防止反序列化重新创建新的对象

非线程同步的方法不能使用

如如果程序开始要加载的资源太多,那么就应该使用懒加载

饿汉式如如果是对象的创建需要配置文件就不适用。

懒加载虽然好,但是静态内部类这种方式会引入编程复杂性

各种的优缺点.
  • 饿汉式的缺点?

  • 懒汉式的缺点?

  • 为什么要用 double- check?不用就不安全吗?

  • 为什么双重检査模式要用 volatile?

    • 新建一个类不是原子操作.
  • 应该选择哪种?

什么是Java内存模型***

解释优点,C++没有的缺点,三兄弟的比较,是一种规范 重排序 可见性 原子性.Happen-before,volatile synchronized

volatile和synchronized 的异同

开销,适用场合,各自的作用

什么是原子操作,有哪些原子操作,生成对象的过程是不是一个原子操作.

新建个空的 Person对象

把这个对象的地址指向p

执行 Person的构造函数

什么是内存可见性

image-20210109194912152

64位的double long写入的时候是原子的嘛

理论上不是,但是商用的不会

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值