一、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虚拟机》