理解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虚拟机》

每一步脚印都要扎得深一点!
原文地址:https://www.cnblogs.com/bloodthirsty/p/12123718.html