概述
在java中应为不同的目的可以将java划分为两种内存模型:gc内存模型。并发内存模型。
gc内存模型
java与c++之间有一堵由内存动态分配与垃圾收集技术所围成的“高墙”。墙外面的人想进去,墙里面的人想出来。
java在执行java程序的过程中会把它管理的内存划分若干个不同功能的数据管理区域。如图:
hotspot中的gc内存模型整体上。分为三部分:栈,堆,程序计数器,他们每一部分有其各自的用途;虚拟机栈保存着每一条线程的执行程序调用堆栈;堆保存着类对象、数组的具体信息;程序计数器保存着每一条线程下一次执行指令位置。这三块区域中栈和程序计数器是线程私有的。也就是说每一个线程拥有其独立的栈和程序计数器。我们可以看看具体结构:
虚拟机/本地方法栈
在栈中,会为每一个线程创建一个栈。线程越多,栈的内存使用越大。对于每一个线程栈。当一个方法在线程中执行的时候,会在线程栈中创建一个栈帧(stack frame),用于存放该方法的上下文(局部变量表、操作数栈、方法返回地址等等)。每一个方法从调用到执行完毕的过程,就是对应着一个栈帧入栈出栈的过程。
本地方法栈与虚拟机栈发挥的作用是类似的,他们之间的区别不过是虚拟机栈为虚拟机执行java(字节码)服务的,而本地方法栈是为虚拟机执行native方法服务的。
方法区/堆
在hotspot的实现中,方法区就是在堆中称为永久代的堆区域。几乎所有的对象/数组的内存空间都在堆上(有少部分在栈上)。在gc管理中,将虚拟机堆分为永久代、老年代、新生代。通过名字我们可以知道一个对象新建一般在新生代。经过几轮的gc。还存活的对象会被移到老年代。永久代用来保存类信息、代码段等几乎不会变的数据。堆中的所有数据是线程共享的。
新生代:应为gc具体实现的优化的原因。hotspot又将新生代划分为一个eden区和两个survivor区。每一次新生代gc时候。只用到一个eden区,一个survivor区。新生代一般的gc策略为mark-copy。
老年代:当新生代中的对象经过若干轮gc后还存活/或survisor在gc内存不够的时候。会把当前对象移动到老年代。老年代一般gc策略为mark-compact。
永久代:永久代一般可以不参与gc。应为其中保存的是一些代码/常量数据/类信息。在永久代gc。清楚的是类信息以及常量池。
程序计数器
如同其名称一样。程序计数器用于记录某个线程下次执行指令位置。程序计数器也是线程私有的。
并发内存模型
java试图定义一个Java内存模型(java memory model jmm)来屏蔽掉各种硬件/操作系统的内存访问差异,以实现让java程序在各个平台下都能达到一致的内存访问效果。java内存模型主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。模型图如下:
java并发内存模型java内存模型中规定了所有变量都存贮到主内存(如虚拟机物理内存中的一部分)中。每一个线程都有一个自己的工作内存(如cpu中的高速缓存)。线程中的工作内存保存了该线程使用到的变量的主内存的副本拷贝。线程对变量的所有操作(读取、赋值等)必须在该线程的工作内存中进行。不同线程之间无法直接访问对方工作内存中变量。线程间变量的值传递均需要通过主内存来完成。
关于主内存与工作内存之间的交互协议,即一个变量如何从主内存拷贝到工作内存。如何从工作内存同步到主内存中的实现细节。java内存模型定义了8种操作来完成。这8种操作每一种都是原子操作。8种操作如下:
- lock(锁定):作用于主内存,它把一个变量标记为一条线程独占状态;
- unlock(解锁):作用于主内存,它将一个处于锁定状态的变量释放出来,释放后的变量才能够被其他线程锁定;
- read(读取):作用于主内存,它把变量值从主内存传送到线程的工作内存中,以便随后的load动作使用;
- load(载入):作用于工作内存,它把read操作的值放入工作内存中的变量副本中;
- use(使用):作用于工作内存,它把工作内存中的值传递给执行引擎,每当虚拟机遇到一个需要使用这个变量的指令时候,将会执行这个动作;
- assign(赋值):作用于工作内存,它把从执行引擎获取的值赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的指令时候,执行该操作;
- store(存储):作用于工作内存,它把工作内存中的一个变量传送给主内存中,以备随后的write操作使用;
- write(写入):作用于主内存,它把store传送值放到主内存中的变量中。
volatile的理解
volatile作用可简单归类为两类:
- 轻量级变量同步(一写多读情况)
- 阻止指令重排优化
轻量级变量同步(一写多读情况)
通过上面的两种内存模型的了解,我们会了解到每个线程对变量的操作,都是在工作内存中,然后通过主内存进行同步。对于普通的变量,如果不主动拉取主内存中的最新变量。当前线程永远取的旧值,举个很简单的例子,对于某个开关变量,一个线程循环读,一个线程某个时间点写。这时候,读线程将是个死循环。
import java.io.IOException;
/**
* User: Rudy Tan
* Date: 2017/11/24
*/
public class App {
public static long isRun = 0;
public static void main(String[] args) throws IOException, InterruptedException {
// 读线程
new Thread(){
public void run(){
do {
System.out.println("check stop");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}while (isRun >= 1);
System.out.println("read thread is stop");
}
}.start();
// 写线程
new Thread(){
public void run(){
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
isRun = 1;
System.out.println("write thread is stop");
}
}.start();
}
}
这个时候读线程会一直循环,无法获取写线程最新修改的值。也就是并发中的不可见性,如果对变量"isRun"添加 volatile。 这个时候,当写线程在自己的工作线程中修改了该值后,立马会同步到主内存中,每次读线程,都会从主内存中读取"isRun"变量。
通过volatile关键词,我们可以达到针对一写多读的线程的轻量级同步。与关键词synchronized区别是,volatile并不是原子性的,什么意思呢?就是volatile针对多写多读的线程同步,不能保证线程的同步。
阻止指令重排优化
引用《深入理解java虚拟机》中一段代码
多个线程对同一个变量的操作,顺序是无序的,"initialized=true"有可能由于指令优化,而提前执行了,导致B线程错误的退出循环,执行下面没有初始化好的代码。
这里有一篇文章讲得不错可以参考一下:
Java内存访问重排序的研究:https://tech.meituan.com/java-memory-reordering.html
都看到这里了,成神之路上,要不要一起?
微信公众号rudy_tan_home
网友评论