• 理解JMM及volatile关键字


    一、Java内存区域

    从《深入理解Java虚拟机》一书中知道

    1. 程序计数器

       当前线程的行号指示器,JVM多线程的方式,导致了线程在被挂起到重新获取执行权时,需要知道上次挂起的地方在哪。在JVM中,

    通过程序计数器来记录字节码的执行位置。程序计数器具有隔离性,为线程私有。此区域不会发生OOM。

    2. Java虚拟机栈

      Java虚拟机栈描述的是Java方法执行的内存模型:每一个方法执行时将创建一个栈帧,存储局部变量表、方法出口等信息。每一个方

    法从调用到执行完成,对应的是栈帧的入栈出栈的过程。

      局部变量存储基本类型、对象引用和returnAddress类型。局部变量包括boolean、byte、char、short、int、float、long、double,其中

    long和double占两个局部变量空间,其余的占一个。对象引用可以是对象的引用指针,也可以是对象的句柄或者与此对象相关的地址。

      Java虚拟机栈为线程私有。

    3. 本地方法栈

           线程私有,这部分存放虚拟机调用的Native方法,一般情况下,我们无需关心。

    4. Java堆

      Java堆的唯一目的就是存储对象实例,是线程的共享区域。

      Java堆是垃圾收集器管理的主要区域,因此又称为“GC堆”。从内存回收的角度,又分为:新生代和老年代,再细致一点,又分为:

    Eden空间、From Survivor空间、To Survivor空间。如果堆中没有内存完成实例分配,并且堆无法扩展,将会OOM。

    5. 方法区

      方法区用于存储类信息、常量、静态变量等数据,也是线程共享的内存区域,区别于堆,有个别名叫“非堆”。

      HotSpot虚拟机的设计团队将GC分代收集扩展至方法区,使用永久代来实现方法区,所以,很多人也称之为“永久代”,本质并不等价。

    二、Java内存模型

      Java内存模型(Java Memory Model,简称JMM)是一种规范,主要目标是定义程序中各个变量的访问规则。此处的变量指的是线程的

    共享变量。

      JMM规定所有的变量都存储在主内存中,每条线程都有自己的工作内存,线程的工作内存中保存了该线程使用到的主内存的变量副本

    拷贝。线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存的变量。

     1. 内存间的原子操作

    • lock(锁定):作用于主内存的变量,把它标识为一条线程独占的状态。
    • unlock(解锁):作用于主内存的变量,把一个处于锁定状态的锁释放。
    • read(读取):作用主内存的变量,将主内存的变量的值传输到工作内存中。
    • load(载入):作用于工作内存的变量,它把read获取到的值放入工作内存的变量副本中。
    • use(使用):作用于工作内存的变量,将工作内存中的变量值传递给执行引擎。
    • assign(赋值):作用于工作内存的变量,把一个从执行引擎接收到的值赋给工作内存的变量。
    • store(存储):作用于工作内存的变量,把工作内存中的变量值传送到主内存中。
    • write(写入):作用于主内存的变量,把store操作从工作内存中得到的变量值更新至主内存的变量中。

    2. 指令重排序

      指令重排是对CPU的优化,其实可以理解为“压榨”计算机运算能力,或者忙里偷闲。

    来看一个例子:

    package com.darchrow.test.reorder;
    
    public class ReorderExample {
    
        public static boolean flag =false;
    
        public static int a =0;
    
        static class  ReadThread extends Thread{
            @Override
            public void run() {
                if(flag){ // 1
                    System.out.println(a == 0 ? "指令重排序!": a); // 2
                }
                System.out.println("read is over");
            }
        }
    
        static class WriteThread extends Thread{
            @Override
            public void run() {
                a =1; // 3
                flag =true; // 4
                System.out.println("write is over");
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            Thread t1 =new ReadThread();
            Thread t2 =new WriteThread();
            t1.start();
            t2.start();
            Thread.sleep(1000);
            System.out.println("main is over");
        }
    
    
    }

    这段程序可能不按预期执行,结果可能会这样:

    WriteThread:                        ReadThread:
    1:flag:true                        1:flag:true
                                        2:a:0        指令重排序!
    2:a:1    这时才写入主内存    

    3、4的操作相互无依赖,可能发生重排序。

    三、volatile

    JMM如何实现volatile的可见性:

    1. read、load、use动作必须连续出现,保证任何一个工作内存中对volatile修饰的变量的读必先强制刷新主内存最新值

    2. assign、store、write动作必须连续出现,保证任何一个工作内存中对volatile修饰的变量的写必须立刻同步到主内存中

    3. 禁止指令重排序

    四、最后看个单例模式

    代码1

    package com.darchrow.test.singleton;
    
    public class DoubleCheckSingleton {
    
        public static DoubleCheckSingleton instance;
    
        public DoubleCheckSingleton(){
        }
    
        public DoubleCheckSingleton getInstance(){
            synchronized (DoubleCheckSingleton.class){
                if(null == instance){
                    instance =new DoubleCheckSingleton();
                }
            }
            return instance;
        }
    }

    这段代码通过synchronized互斥锁实现,会存在多个线程争夺getInstantce()效率的问题。当然也会发生指令重排,

    但指令重排发生在获得锁的那个单线程里,所以不会有什么问题。

    代码2

    package com.darchrow.test.singleton;
    
    public class DoubleCheckSingleton {
    
        public static volatile DoubleCheckSingleton instance;
    
        private DoubleCheckSingleton(){
        }
    
        public DoubleCheckSingleton getInstance(){
            if(null == instance){ // 1
                synchronized (DoubleCheckSingleton.class){
                    if(null == instance){
                        instance =new DoubleCheckSingleton();// 2
                    }
                }
            }
            return instance;
        }
    }

    我们加了volatile关键字,

    标1处,解决了多线程争抢锁资源的问题

    标2处,解决了指令重排的问题

    这里说明下标2处的指令重排

    instance =new DoubleCheckSingleton();这段代码可以分解成以下3步完成(伪代码):

    memory = allocate(); //1.分配对象内存空间
    init(memory); //2.初始化对象
    instance = memory; //3.instance指向刚分配的内存地址

    2、3是可能重排序的

    memory = allocate(); //1.分配对象内存空间
    instance = memory; //2.instance指向刚分配的内存地址
    init(memory); //3.初始化对象

    指令重排只会保证串行语义的一致性,但不会关心多线程语义的一致性。所以当一条线程读取instance不为null时,

    并不代表instance初始化完成,这会造成线程安全问题。volatile禁止了修饰变量的指令重排。

    参考:

    https://www.jianshu.com/p/6dd0c33e7756

    周志明--《深入理解Java虚拟机》

    每一步脚印都要扎得深一点!
  • 相关阅读:
    nodejs使用superagent写爬虫dns超时
    react部署nginx刷新路由404
    ubuntu安装mongodb添加账户以及远程连接
    laravel使用layui富文本编辑器layedit上传图片419解决办法
    编写前端统计网页流量,来源,停留时间等
    laravel模版共用数据解决方法
    解决MySQL导入中文乱码
    yii2 jui DatePicker widget 设置显示默认时间
    装饰器
    python函数计时器(通过装饰器实现)
  • 原文地址:https://www.cnblogs.com/bloodthirsty/p/12123718.html
Copyright © 2020-2023  润新知