Java内存模型-底层原理

底层原理

Java程序的编译与运行

  1. 最开始,我们编写的Java代码,是*.java文件
  2. 在变异(javac命令)后,从刚才的*.java文件会变成一个新的Java字节码文件(*.class
  3. JVM会执行刚才生成的字节码文件(*.class),并把字节码文件转化为机器指令
  4. 机器指令可以直接在CPU上执行运行,也就是最终的程序执行

JVM实现会带来不同的“翻译”,不同的CPU平台机器指令有千差万别,无法保证并发安全的效果一致

 

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

这三个是截然不同的概念,下面会一一进行讲解

1.1、JVM内存结构

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

  • 堆:整个运行区最大的,new创建的实例对象,都存储在堆中,运行时动态分配
  • 虚拟机栈:对象的引用,存储在java栈中,编译期就确定了大小
  • 方法区:储存static静态变量,常量信息等,还有永久引用(static修饰的class),运行时常量池也是存储在方法区
  • 本地方法栈:Native方法
  • 程序计数器:保存当前线程执行的字节码的行号数,协助上下文切换

1.2、Java内存模型

Java内存模型,和Java的并发编程有关

1.3、Java对象模型

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

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

 

2、JMM是什么

2.1、为什么需要JMM(Java内存模型)

C语言不存在内存模型的概念,很多操作依赖处理器,不同的处理器处理的结果不一样。无法保证并发安全。JMM是一个标准规范,让多线程运行的结果可预期

JMM是一组规范,需要各个JVM的实现来遵守JMM规范,以便于开发者可以利用这些规范,更方便的开发多线程程序

如果没有JMM内存模型来规范,那么JVM可能根据不同规则的重排序之后,导致不同的虚拟机上的运行结果的不一样,那是很大的问题

2.2、关键字的原理

帮助我们实现了内存栅栏,简单理解就是主内存和线程内存之间的同步。我们只需要使用关键字就可以达到效果,而不用自己去实现内存栅栏。

volatile、synchronize、Lock等的原理都是JMM

3、重排序

代码的实际执行顺序代码在Java文件中的顺序不一致,代码指令并不是严格按照代码语句顺序执行的,他们的顺序被改变了,这就是重排序。

重排序的好处:提高运行速度和处理速度

/**
 * 演示重排序
 * 直达达到某个条件才停止,测试小概率事件
 */
public class OutOfOrderExcution {
    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(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();

            if (x == 1 && y == 1){
            // if (x == 0 && y == 0){
                System.out.println("i="+i+", x="+x+", y="+y);
                break;
            }else {
                System.out.println("i="+i+", x="+x+", y="+y);
            }

        }
    }

}

这4行代码的执行顺序界定了最终x和y的结果,一共有3种情况:

  • a=1;x=b(0);b=1;y=a(1),最终结果是x=0,y=1
  • b=1;y=a(0);a=1;x=b(1),最终结果是x=1,y=0
  • b=1;a=1;x=b(1);y=a(1),最终结果是x=1,y=1

一定只有这3中情况吗?会不会出现第四种情况呢?

为什么出现了0,0的情况呢?那是因为重排序发生了,4行代码的执行顺序为:y=a;a=1;x=b;b=1;

内存中“重排序”:线程A的修改线程B却看不到,引出可见性问题。修改后没有及时写回主存造成的现象。

 

4、可见性

4.1、什么是可见性问题

代码演示

可以使用volatile关键字解决这个问题,他会在修改本地缓存后刷新到主内存中,这样其他线程获取的就是修改后的值

4.2、为什么会出现可见性

  • 因为cpu有多层缓存,越靠近cpu的容量越小,但是速度越快。
  • 线程中的可见性是由于多级缓存造成的
  • 每个核心都会将自己需要的数据读到独占缓存中,数据修改后也是写入到缓存中,然后等待刷入到主存中。所以会导致有些核心读取到一个过期的值

4.3、JMM的抽象:主内存和本地内存

  • Java作为高级语言,屏蔽了这些底层细节,用JMM定义了一套读写内存数据的规范,虽然我们不再关心一级缓存和二级缓存的问题,但是,JMM抽象了主内存和本地内存的概念。
  • 这里说的本地内存并不是真的是一块给每个线程分配的内存,而是,JMM的一个抽象,是对于寄存器、一级缓存、二级缓存的抽象

JMM有以下规定:

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

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

4.4、Happens-Before原则

4.4.1、什么是Happens-Before

  • Happens-Before是用来解决可见性问题的:在时间上,动作A发生字董总B之前,B保证能看见A,这就是Happens-Before
  • 两个操作可以用Happens-Before来确定他们的执行顺序:如果一个操作Happens-Before于另一个操作,那么我们说第一个操作对于第二个操作是可见的
  • Happens-Beforea就是想要说明一个可见性的问题

4.4.2、什么不是Happens-Before

  • 两个线程没有相互配合的机制,所以代码X和Y执行结果并不能保证总被别人看到,这就不是Happens-Before

4.4.3、Happens-Before规则有哪些

  • 单线程原则
  • 锁操作(synchronize和Lock)
  • volatile变量
  • 线程启动
  • 线程join
  • 传递性
  • 中断
  • 构造方法
  • 工具类

近朱者赤:给一个变量加了volatile,不仅改变量被影响,读取变量之前的操作也是可见的,可以实现轻量级同步

4.5、volatile关键字

  • volatile是一种同步机制,比synchronize或者Lock相关类更轻量,因为使用volatile并不会发送上下文切换等开销很大的行为
  • 如果一个变量被修饰为volatile,那么JVM就知道这个变量可能被并发修改
  • 因为开销小,相应的能力也小,虽说volatile也是用来同步线程安全的,但是volatile做不到synchronize那样的原子保护,volatile仅在很有限的场景下才能发挥作用

4.5.1、volatile不适用于a++操作

/**
 * 不适用于volatile
 */
public class NoVolatile implements Runnable{
    volatile int a;
    AtomicInteger readA = new AtomicInteger();

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            readA.incrementAndGet();
            a++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        NoVolatile noVolatile = new NoVolatile();
        Thread thread1 = new Thread(noVolatile);
        Thread thread2 = new Thread(noVolatile);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();

        System.out.println(noVolatile.a);
        System.out.println(noVolatile.readA);
    }
}

4.5.2、适用的场景 

  • 适合场景1:boolean flag,如果一个共享变量自始至终只被各个线程赋值,而没有其他操作,那么可以使用volatile代替synchronize或者代替原子变量操作。因为赋值操作是原子操作
/**
 * 适用于volatile
 */
public class UseVolatile1 implements Runnable{
    volatile boolean done = false;
    AtomicInteger readA = new AtomicInteger();

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            readA.incrementAndGet();
            setDone();
        }
    }

    private void setDone() {
        done = true;
    }

    public static void main(String[] args) throws InterruptedException {
        UseVolatile1 noVolatile = new UseVolatile1();
        Thread thread1 = new Thread(noVolatile);
        Thread thread2 = new Thread(noVolatile);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();

        System.out.println(noVolatile.done);
        System.out.println(noVolatile.readA);
    }
}
/**
 * 适用于volatile
 */
public class NoVolatile2 implements Runnable{
    volatile boolean done = false;
    AtomicInteger readA = new AtomicInteger();

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            readA.incrementAndGet();
            flipDone();
        }
    }

    private void flipDone() {
        done = !done;
    }

    public static void main(String[] args) throws InterruptedException {
        NoVolatile2 noVolatile = new NoVolatile2();
        Thread thread1 = new Thread(noVolatile);
        Thread thread2 = new Thread(noVolatile);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();

        System.out.println(noVolatile.done);
        System.out.println(noVolatile.readA);
    }
}

改变值不依赖他原来的值,就可以适用于volatile关键字

  • 适用场合2:作为刷新触发器

4.5.3、volatile的两点作用

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

4.5.4、volatile和synchronize的关系

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

4.5.5、小结

  • volatile修饰符适用于以下场景:某个属性被多个线程共享,其中有一个线程对此属性进行了修改,其他线程可以立即得到修改后的值,比如,boolean flag;作为触发器,实现轻量级同步
  • volatile属性的读写操作都是无锁的,他不能代替synchronize,因为他没有提供原子性互斥性。因为无所,不需要花费时间在获取锁和释放锁上,所以说它是低成本
  • volatile只能作用于属性,我们用volatile修饰属性,这样jvm就不会对这个属性进行重排序
  • volatile提供了可见性,任何一个线程对其修改将立马对其他线程可见,volatile属性不会被线程缓存,始终都是从主存中读取
  • volatile提供了happens-before保证,对volatile变量v的写入happens-before所有其他线程后续对v的读操作
  • volatile可以使得long和double的赋值是原子

4.6、能保证可见性的措施

  • 除了volatile可以让变量保证可见性外,synchronize、Lock、并发集合、Thread.join()和Thread.start()等都可以保证可见性
  • 具体可以参考happens-before

4.7、对synchronize可见性的正确理解

  • synchronize不仅保证了原子性,还保证了可见性
  • synchronize不仅让被保护的代码安全,还近朱者赤(让被锁住之前的代码也可见)

5、原子性

5.1、什么是原子性

  • 一系列的操作,要么全部都执行成功,要么全部不执行,不会出现执行一般的情况,是不可能分割的

5.2、Java中原子操作有哪些

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

5.3、long和double的原子性

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

32位机器上,long和double不是原子操作,在64位机器上,long和double是原子操作

实际开发中,商用java虚拟机不会出现这种情况,因为已经对这个做过处理

5.4、原子性 + 原子性 != 原子性

  • 两个原子操作组合在一起,并不能保证整体依然具有原子性

6、常见问题

6.1、单例模式8中写法,单例和并发的关系

单例模式可以节省内存和计算保证结果正确,方便管理

/**
 * 1.饿汉式(静态常量)  (可用)
 * 类加载的时候就会实例化
 */
public class Singleton1 {

    private final static Singleton1 INSTANCE = new Singleton1();

    private Singleton1(){}

    public static Singleton1 getINSTANCE() {
        return INSTANCE;
    }
}

/**
 * 2.饿汉式(静态代码块)  (可用)
 * 类加载的时候就会实例化
 */
public class Singleton2 {

    private final static Singleton2 INSTANCE;

    static {
        INSTANCE = new Singleton2();
    }

    private Singleton2(){}

    public static Singleton2 getINSTANCE() {
        return INSTANCE;
    }
}
/**
 * 3.懒汉式(线程不安全)  (不可用)
 */
public class Singleton3 {

    private static Singleton3 INSTANCE;

    private Singleton3(){}

    public static Singleton3 getINSTANCE() {
        //如果两个线程同时进入,判断都为null,那么下面创建就会多次
        if (INSTANCE == null){
            INSTANCE = new Singleton3();
        }
        return INSTANCE;
    }
}

/**
 * 4.懒汉式(线程安全)  (不推荐)
 */
public class Singleton4 {

    private static Singleton4 INSTANCE;

    private Singleton4(){}

    public synchronized static Singleton4 getINSTANCE() {
        //synchronized保护,那么两个线程无法同时进入,导致多次创建的问题。但是效率太低
        if (INSTANCE == null){
            INSTANCE = new Singleton4();
        }
        return INSTANCE;
    }
}
/**
 * 5.懒汉式(线程不安全)  (不推荐)
 */
public class Singleton5 {

    private static Singleton5 INSTANCE;

    private Singleton5(){}

    public static Singleton5 getINSTANCE() {
        if (INSTANCE == null){
            //如果两个线程都到这里了,虽然创建上锁了,但是依旧会多次创建
            synchronized (Singleton5.class) {
                INSTANCE = new Singleton5();
            }
        }
        return INSTANCE;
    }
}

/**
 * 6.懒汉式(线程安全)  (推荐)
 * 双重检查   推荐面试使用
 */
public class Singleton6 {

    //新建对象实际上有3个步骤
    /**
     * 1.创建一个空的对象
     * 2.调用对象的构造方法
     * 3.将实例赋值给引用
     * 如果CPU做了重排序,那么就会发生NPE
     */
    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;
    }

}
/**
 * 7.静态内部类  (可用)
 */
public class Singleton7 {


    public Integer a;

    private Singleton7(){
    }

    //内部类只有在
    private static class SingletonInstance{
        private static final Singleton7 INSTANCE = new Singleton7();
    }

    public static Singleton7 getINSTANCE() {
        return SingletonInstance.INSTANCE;
    }

}
/**
 * 8.枚举单例  (可用)   防止反序列化
 * 这个类最简单
 */
public enum  Singleton8 {

    INSTANCE;

    private void whatever() {

    }

}

最好使用枚举

6.2、单例模式面试常见问题

  • 饿汉式缺点:如果不适用这个实例,也会加载,会有浪费的问题
  • 懒汉式的缺点:容易写成线程不安全
  • 为什么要用双重锁?不用线程不安全吗?不使用线程不安全的,
  • 为什么双重锁检查要用volatile?因为new为三个操作,创建空对象,构造方法,赋值。如果顺序无法保证,就会有空指针问题。因为添加了volatile,对象被创建,其他线程也一定可以查看到,可见性问题
  • 使用哪种实现方式最好?枚举

6.3、什么是Java内存模型

为啥要有->JVM内存结构、Java内存模型、Java对象模型的区别->Java内存模型是一个规范,三个点->可见性对内存的抽象->happens-before原则->volatile关键字->适用场合->和synchronize的关系->原子性(哪些操作是原子的)

6.4、volatile和synchronize的异同

volatile可以看作是轻量级的synchronize,volatile无法实现原子性

6.5、什么是原子操作?Java有哪些是原子操作?生成对象使不是原子操作?

生成对象不是原子操作,他是三个步骤:1.新建一个空的person对象 2.吧这个对象的地址指向p 3.执行person的构造函数。这期间的2,3步操作顺序需要看是否被重排序

6.6、什么是内存可见性

层层缓存导致

6.7、64位是不是原子操作

Java并没有说他们是原子的,所以可能会出现错位的情况,但是商用虚拟机中做了操作,不需要我们来关心这个

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值