🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
[TOC] # 内存划分 ![](https://img.kancloud.cn/4b/57/4b5712afa06d951acc088770bce0ed56_674x314.png) # java对象在内存的分配 ~~~java public class Student { private String name; private static Birthday birthday = new Birthday(); public Student(String name) { this.name = name; } public static void main(String[] args) { Student s = new Student("zhangsan"); int age = 10; System.out.println(age); } } class Birthday { private int year = 2010; private int month = 10; private int day = 1; } ~~~ ![](https://img.kancloud.cn/63/71/6371ddd8bc0eae5e73a5678a1d560ded_669x433.png) # 从内存区域来分析 * **虚拟机栈**:只存放局部变量 * **堆**:存储对象的实例 * **方法区**:存放Class信息和常量信息。 # 从变量的角度来分析 * **局部变量:**存放在虚拟机栈中(具体应为\[栈->栈帧->局部变量表\]) * 基本类型的值直接存在栈中。如age=10 * 如果是对象的实例,则只存储对象实例的引用。如s=ref * **实例变量:**存放在堆中的对象实例中。如Student的实例变量 name=ref * **静态变量:**存放在方法区中的常量池中。如Student.class中的birthday=ref。 * 如果常量的类型是对象的实例则只存储对象实例的引用地址 > 通过变量的角度来分析,我们就可以了解为什么静态变量不用new就能调用,而实例变量必须new出对象,才能调用。 # 内存模型 ![](https://img.kancloud.cn/62/6e/626ed3424687763229e5f2b070e4c4e7_1365x575.png) java的并发采用的是共享内存模型(而非消息传递模型) Java线程之间的通信由Java内存模型(JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见. JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本 ![](https://img.kancloud.cn/66/4f/664f1a8e9c40a892c007f8a4fbfa7948_408x362.png) 图中的共享变量为:实例变量和静态变量。(局部变量是线程私有的不存在竞争) 从图中看,线程A和线程B进行通讯必须通过主内存 1. 线程A修改了一个共享变量,并刷新到主内存中 2. 线程B从主内存读取A修改过的共享变量 **注意:** 1. 线程对共享变量的所有操作都必须在自己的本地内存中进行,不能直接从主内存中读写。 2. 不同线程之间无法直接访问其它线程的本地内存,线程间的变量值的传递,必须通过主内存来完成。 # 指令重排序 指令重排序是JVM为了优化指令,提高程序运行效率,编译器、处理器也遵循这样一个目标. 重排序分三种类型: ![](https://img.kancloud.cn/a0/19/a019948058d3daa64ff24c6d7948ebb3_536x66.png) 1. 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。 2. 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。 3. 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。 # 内存可见性 内存可见性简单描述:当主内存中的一个共享变量在多个线程的本地内存中都存在副本,如果一个线程修改共享变量,其它线程也应该能看到被修改后的值。 > **要实现共享变量的可见性,必须实现两点:** 1. 线程修改后的共享变量值能够及时的从工作内存刷新到主内存中。 2. 其它线程能够及时把共享变量的最新值从主内存更新到自己的本地内存中。 # happens-before规则 JMM使用happens-before的概念来阐述操作之间的内存可见性。在JMM中,如果一个操作执行的结果**需要对**另一个操作**可见**,那么这两个操作就必须存在happens-before关系。 **happens-before规则如下:** 1. 程序顺序规则:一个线程中的每个操作,happens- before 于该线程中的任意后续操作。 2. 监视器锁规则:对一个监视器锁的解锁,happens- before 于随后对这个监视器锁的加锁。 3. volatile变量规则:对一个volatile域的写,happens- before 于任意后续对这个volatile域的读。 4. 传递性:如果A happens- before B,且B happens- before C,那么A happens- before C。 > 注意,两个操作之间具有happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行!happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前。 # as-if-serial as-if-serial定义重排序的规则。 as-if-serial语义的意思:不管怎么重排序,程序的执行结果都不能被改变。编译器、runtime和处理器都必须遵守as-if-serial规则。 ~~~java int a=2; //A int b=3; //B int c=a*b; //C ~~~ ![](https://img.kancloud.cn/8f/74/8f74b4bb1190da25154080d938703bc9_385x145.png) > **注意:** 1. 重排序不会给单线程程序带来内存可见性问题 2. 多线程程序并发交叉执行时,重排序可能会造成内存可见性问题 上面程序的happens-before的关系: 1. A happens-before B 2. B happens-before C 3. A happens-before C (happens-before的传递性) 在这里A happens-before B, 但实际执行B可以排在A之前执行。JMM仅仅要求前一个操作对后一个操作可见,且前一个操作按顺序排在第二个操作之前。 > 这里A的执行结果**不需要**对操作B**可见** # 可见性与原子性    可见性:一个线程对共享变量的修改,更够及时的被其他线程看到    原子性:即不可再分了,不能分为多步操作。比如赋值或者return。比如"a = 1;"和 "return a;"这样的操作都具有原子性。类似"a += b"这样的操作不具有原子性,在某些JVM中"a += b"可能要经过这样三个步骤: ① 取出a和b ② 计算a+b ③ 将计算结果写入内存 1. Synchronized:保证可见性和原子性     Synchronized能够实现原子性和可见性;在Java内存模型中,synchronized规定,线程在加锁时,先清空工作内存→在主内存中拷贝最新变量的副本到工作内存→执行完代码→将更改后的共享变量的值刷新到主内存中→释放互斥锁。 2. Volatile:保证可见性,但不保证操作的原子性     Volatile实现内存可见性是通过store和load指令完成的;也就是对volatile变量执行写操作时,会在写操作后加入一条store指令,即强迫线程将最新的值刷新到主内存中;而在读操作时,会加入一条load指令,即强迫从主内存中读入变量的值。但**volatile不保证volatile变量的原子性 ** **Synchronized和Volatile的比较** 1. Synchronized保证内存可见性和操作的原子性 2. Volatile只能保证内存可见性 3. Volatile不需要加锁,比Synchronized更轻量级,并不会阻塞线程(volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。) 4. volatile标记的变量不会被编译器优化,而synchronized标记的变量可以被编译器优化(如编译器重排序的优化). 5. volatile是变量修饰符,仅能用于变量,而synchronized是一个方法或块的修饰符。