Java 内存模型
从 Java 代码到 CPU 执行
- Java 代码(IDE)
- javac 命令将源代码编译成 *.class (字节码)文件。
- JVM 会根据操作系统和 CPU 平台的差异,将同样的字节码解释成不同的机器指令,无法保证并发安全的效果一致。
- CPU 运行机器指令(程序的执行)。
基于并发安全性的考虑,需要制定一套规范、原则,用统一转化过程和结果,保证程序并发执行的安全性。
JVM 内存结构
Java 虚拟机的运行时区域相关。
方法区(Method Area)
是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
堆(heap)
Java 堆是线程共享的。在一般情况下,堆可以说是 Java 内存中最大的内存区域。其存放了对象实例,几乎所有的对象实例在这里存储。(这里说是几乎,是因为 JIT 优化的存在,可能会有对象不在堆上分配,而在栈上进行分配)。
Java 虚拟机栈(VM stack)
用于作用于方法执行的一块 Java 内存区域。每个方法在执行的同时都会创建一个栈帧(Stack Framel):用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
本地方法栈(Native Method Stack)
用于执行 native 方法的一块 Java 内存区域,类似于 Java 虚拟机栈。native 方法可以调用 Java 语言之外的其他语言。
程序计数器(Program Counter Register)
为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储。
Java 对象模型
Java 对象在 JVM 中的表现形式有关。因为 Java 是面向对象的,所以每个对象在 JVM 中存储需要遵循一定的结构。
JVM 会给这个类创建一个 instanceKlass,保存在方法区,用来在 JVM 层表示该 Java 类。
当我们在 Java 代码中,使用 new 创建一个对象的时候,JVM 会创建一个 instanceOopDesc 对象并存在堆种,这个对象中包含了对象头以及实例数据。
Java 内存模型
Java 并发编程相关。
JMM(Java Memory Model)
C 语言中不存在内存模型的概念,因此会导致很多弊端,需要一个标准,让多线程运行的结果可预期。
最重要的 3 点内容:重排序、可见性、原子性。
JMM 是一组规范,需要 JVM 的实现来遵守 JMM 规范,以便于开发者可以利用这些规范,更方便地开发多线程程序。
1* 程序行为依赖处理器本身的内存一致性模型,因此不同处理器结果不一样(不同的处理器来自于不同的厂商)。
2* 无法保证并发安全。
3* 如果没有JMM内存模型来规范,不同的JVM的不同规则的重排序之后,导致不同的虚拟机上运行的结果不一样。
4* 需要一个标准,让多线程运行的结果可预期。
JMM 同时也是工具类和关键字的原理:volatile、synchronized、Lock等……
如果没有 JMM,那就需要我们自己制定什么时候用内存栅栏 等…,会增加多线程开发的复杂度。
重排序
在线程内部的代码实际执行顺序和代码在 Java 文件中的不一致,代码指令并不是严格按照代码语句顺序执行的,他们的顺序被改变了,这就是重排序。这里被颠倒的是 y=a 和 b=1 这两个语句。
重排序的三种情况
- 编译器优化:包括 JVM、JIT 编译器等……
- CPU 指令重排:就算编译器不方法重排,CPU 也可能对指令进行重排,CPU 的优化行为和编译器优化很类似,是通过乱序执行的技术,来提高执行效率。
- 内存的 “重排序”:内存系统本身不存在重排序,但是内存会带来看上去和重排序一样的效果,线程 A 的修改线程 B 却看不到,引出可见性问题。(主存和线程本地缓存的数据不一致导致的)
重排序惹的祸
1public class OutOfOrderExecutionDemo {
2 private static int x = 0, y = 0;
3 private static int a = 0, b = 0;
4
5 public static void main(String[] args) {
6 Thread t1 = new Thread(() -> {
7 a = 1;
8 x = b;
9 });
10 Thread t2 = new Thread(() -> {
11 b = 1;
12 y = a;
13 });
14 t1.start();
15 t2.start();
16 while (t1.isAlive() || t2.isAlive()) {
17
18 }
19 System.out.println("x = " + x + " ,y = " + y);
20 }
21}
会有三种运行结果
11. a=1;x=b(0); b=1;y=a(1) 结果: x=0,y=1
22. b=1;y=a(0); a=1;x=b(1) 结果: x=1,y=0
33. b=1;a=1; x=b(1);y=a(1) 结果: x=1,y=1
重排序结果分析:会出现 x=0,y=0? 那是因为重排序,4 行代码的执行顺序的其中一种可能是:
1第261820次(0,0)
2
3y = a; -thread1
4a = 1; -thread2
5x = b; -thread2
6b = 1; -thread1
重排序能提高处理速度
重排序会对指令进行优化。
可见性
所有的共享变量存储在主内存中,每个线程有自己的本地内存,而且线程读写共享数据也是通过本地内存交换的,所以才导致了可见性问题。
可见性问题
可见性问题有如下成因:
- CPU 有多级缓存,导致读的数据过期。
1* 高速缓存的容量比主内存小,但是速度仅次于寄存器,所以在 CPU 和 主存之间就多 Cache 层。
2* 线程间的对于共享变量的可见性问题不是直接由多核引起的,而是由多缓存引起的。
3* 如果所有核心都只用一个缓存,那么也就不存在内存可见性问题了。但是通常情况下每个核心都会将自己所需要的数据读到独占缓存中,数据修改后也是写入到缓存中,然后等待刷入到主存中。所以会导致有些核心读取的值是一个过期的值。
1public class FieldVisibility {
2 //volatile可以强制线程每次都读取到最新的值(强制将写线程修改的本地内存数据同步到主内存)
3// volatile int a = 1;
4// volatile int b = 2;
5 int a = 1;
6 int b = 2;
7
8 private void change() {
9 a = 3;
10 b = a;
11 }
12 private void print() {
13 System.out.println("b=" + b + ";a=" + a);
14 }
15
16 public static void main(String[] args) {
17 while (true) {
18 FieldVisibility test = new FieldVisibility();
19 new Thread(new Runnable() {
20 @Override
21 public void run() {
22 try {
23 Thread.sleep(1);
24 } catch (InterruptedException e) {
25 e.printStackTrace();
26 }
27 test.change();
28 }
29 }).start();
30
31 new Thread(new Runnable() {
32 @Override
33 public void run() {
34 try {
35 Thread.sleep(1);
36 } catch (InterruptedException e) {
37 e.printStackTrace();
38 }
39 test.print();
40 }
41 }).start();
42 }
43 }
44}
分析四种情况:
1# 普通情况
2a = 3, b = 2
3a = 1, b = 2
4a = 3, b = 3
5
6# 特殊情况(很罕见,可见性造成的)
7a = 1, b = 3
JMM 的抽象:主内存和本地内存
Java 作为高级语言,屏蔽了这些底层细节,用 JMM 定义了一套读写内存数据的规范,虽然我们不再需要关心一级缓存和二级缓存的问题,而是,JMM 抽象了主内存和本地内存的概念。
这里说的本地内存并不是真的在 CPU 中给每个线程分配的内存,而是 JMM 的一个抽象,是对于寄存器、一级缓存、二级缓存等的抽象(根据 CPU 结构不同进行抽象,把 registers 和 L1 cache 1~3 抽象成为一层,即 “本地缓存”)。
主内存和本地内存的关系
JMM 有以下规定:
- 所有的变量都是存储在主内存中,同时每个线程也有自己独立的工作内存,工作内存中的变量内容是主内存中的拷贝。
- 线程不能直接读写主内存中的变量,而是只能操作自己工作内存中的变量,然后再同步到主内存中。
- 主内存是多个线程共享的,但线程不共享工作内存,如果线程间需要通信,必须借助主内存中转来完成。
Happens-Before 原则
hapepens-before 规定了一系列的可见性的原则。
单线程原则
在单线程的情况下,后执行的代码一定能看到先执行的代码都做了哪些操作和修改。
锁操作(synchronized、Lock)
线程 A 加锁,之后解锁,线程 B 加锁,线程 B 对于线线程 A 加锁之后的所有操作都是可见的。
volatile 变量
被 volatile 修饰的变量可以保证在写线程完成操作之后,读线程一定能看到。
线程启动
子线程被启动之后可以看到子线程未被启动之前,主线程的所有操作结果。
线程 join
保证 join()后面的语句可以看得到 join()之前所执行的所有操作。(防止 join()后面的语句被重排序到了 join()之前)
传递性
第一行代码运行完第二行就能看到,第二行运行完第三行就能看到。所以第一行运行完之后第十行也一定能够看到。
中断
一个线程被其他线程 interrupt 时,那么检测中断(isInterrupted) 或者抛出 InterruptedException 一定能看到。
构造方法
对象构造方法的最后一行指令 happens-before 于 finalize() 方法的第一行指令。(finalize 方法已经不再推荐使用)。就是说 finalize()方法执行的时候一定可以看到 构造方法的最后一行指令。
工具类的 Happens-Before 原则
11. 线程安全的容器get一定能看到在此之前的put等存入动作(即便是不同的线程在操作,依然可以读取到最新的值)。
22. CountDownLatch(latch.countDown()执行之后,latch.awat()才能被唤醒)
33. Semaphore(信号量和CountDownLatch类似)
44. Futrue
55. 线程池
66. CyclicBarrier
volatile 关键字
volatile 是什么
volatile 是一种同步机制,比如 synchronized 或 Lock 相关类更轻量,因为使用 volatile 并不会发生上下文切换等开销很大的行为。
如果一个变量被修饰成 volatile,那么 JVM 就知道这个变量可能会被并发修改。
但是开销小,相应的能力也小,虽然说 volatile 是用来同步的保证线程安全的,但是 volatile 做不到 synchronized 那样的原子保护, volatile 仅在很有限的场景下才能发挥作用。
volatile 不适用场合
- 不适用:a++
volatile 的适用场合
- boolean flag,如果一个共享变量自始至终只被各个线程复制,而没有其他的操作,那么就可以用 volatile 来代替 synchronized 或者代替原子变量,因为赋值自身是有原子性的,而 volatile 又保证了可见性,所以就足以保证线程安全。
- 作为刷新之前变量的触发器
while(!flag){...}
volatile 的作用
- 可见性:读一个 volatile 变量之前,需要先使用相应的本地缓存失效,这样就必须到主内存读取最新只,写一个 volatile 属性会立即刷入到主内存。
- 禁止重排序:解决单例双重锁乱序问题。
volatile 和 synchronized 的关系?
volatile 在这方面可以看做是轻量版的 synchronzied:如果一个共享变量自始至终只被各个线程复制,而没有其他的操作,那么就可以用 volatile 来代替 synchronized 或者代替原子变量,因为赋值自身是有原子性的,而 volatile 又保证了可见性,所以就足以保证线程安全。
用 volatile 修正重排序问题
1import java.util.concurrent.CountDownLatch;
2
3/**
4 * 描述: 演示重排序的现象 “直到达到某个条件才停止”,测试小概率事件
5 */
6public class OutOfOrderExecution {
7
8 private volatile static int x = 0, y = 0;
9 private volatile static int a = 0, b = 0;
10
11 public static void main(String[] args) throws InterruptedException {
12 int i = 0;
13 for (; ; ) {
14 i++;
15 x = 0;
16 y = 0;
17 a = 0;
18 b = 0;
19
20 CountDownLatch latch = new CountDownLatch(1);
21
22 Thread one = new Thread(new Runnable() {
23 @Override
24 public void run() {
25 try {
26// latch.countDown();
27 latch.await();
28 } catch (InterruptedException e) {
29 e.printStackTrace();
30 }
31 a = 1;
32 x = b;
33 }
34 });
35 Thread two = new Thread(new Runnable() {
36 @Override
37 public void run() {
38 try {
39// latch.countDown();
40 latch.await();
41 } catch (InterruptedException e) {
42 e.printStackTrace();
43 }
44 b = 1;
45 y = a;
46 }
47 });
48 two.start();
49 one.start();
50 latch.countDown();
51 one.join();
52 two.join();
53
54 String result = "第" + i + "次(" + x + "," + y + ")";
55 if (x == 0 && y == 0) { //volatile 解决了不会发生 (0,0)的情况
56 System.out.println(result);
57 break;
58 } else {
59 System.out.println(result);
60 }
61 }
62 }
63}
volatile 小结
volatile 修饰符适用于以下场景:某个属性被多个线程共享,其中有一个线程修改了此属性,其他线程可以立即得到修改后的值,比如: boolean flag; 或者作为触发器,实现轻量级同步。
volatile 属性的读写操作都是无锁的,它不能替代 synchronzied,因为它没有提供原子性和互斥性。因为无锁,不需要花费时间在获取锁和释放锁上,所以说它是低成本的。
volatile 只能作用于属性,可以禁止编译器对属性做指令重排序。
volatile 提供了可见性,任何一个线程对其的修改将立马对其他线程可见。 volatile 属性不会被线程缓存,始终从主存中读取。
volatile 提供了 happens-before 保证,对 volatile 变量 v 的写入 Happens-before 保证了所有其他线程后续对 v 的读操作是最新的值。
volatile 可以使得 long 和 double 的赋值是原子的。
能保证可见性的措施
除了 volatile 可以让变量保证可见性外,synchronized、Lock、并发集合、Thread.join()、Thread.start()等都可以保证可见性。
具体看 happens-before 原则的规定。(hapepens-before 规定了一系列的可见性的原则,避免了 CPU 和编译器重排序造成的问题)
synchronized 也具备可见性
synchronized 不仅保证了原子性,还保证了可见性。
由于近朱者赤,synchronzied 还可以有效的保证 synchronzied 之前的代码的可见性。
原子性
一系列的操作,要么全部执行成功,要么全部不执行,不会出现执行一半的情况,是不可分割的。
Java 中的原子操作有哪些?
- 除 long 、double 之外的基本类型 (int、byte、boolean、short、char、float)的赋值操作。
- 所有引用 reference 的赋值操作,不管是 32 位的机器还是 64 位的机器。
- java.concurrent.Atomic.* 包中所有类的原子操作。
long 和 double 的原子性
对于 64 位的值的写入,可以分为两个 32 位的操作进行写入,所以会导致读取错误。
推荐使用使用 volatile 修饰变量可以让 long、double 具备原子性。具备线程安全性。
在 32 位操作系统上 JVM,long、double 的操作不是原子的,但是在 64 位的 JVM 上是原子的。
商用 Java 虚拟机中自动保证原子性。
原子操作 + 原子操作 != 原子操作
简单地把原子操作组合在一起,并不能保证整个操作依然是具有原子性的。
总结
JMM 应用实例:单例模式 8 种写法,单例和并发的关系。
单例模式的优点如下:
1* 节省内存和计算
2* 保证结果正确
3* 方法管理
单例模式的适用场景
1* 无状态的工具类:日志工具类、只需要调用它记录日志信息,除此之外,并不需要在它的实例对象上存储任何状态,这个时候就只需要一个实例对象即可。
2* 全局信息类:统计网站访问次数的类。
- 饿汉式(静态常量)线程安全)
1/**
2 * 描述: 饿汉式(静态常量)(线程安全)
3 */
4public class Singleton1 {
5
6 private final static Singleton1 INSTANCE = new Singleton1();
7
8 private Singleton1() {
9
10 }
11 public static Singleton1 getInstance() {
12 return INSTANCE;
13 }
14}
- 饿汉式(静态代码块)(线程安全)
1/**
2 * 描述: 饿汉式(静态代码块)(线程安全)
3 */
4public class Singleton2 {
5
6 private final static Singleton2 INSTANCE;
7
8 static {
9 INSTANCE = new Singleton2();
10 }
11
12 private Singleton2() {
13 }
14
15 public static Singleton2 getInstance() {
16 return INSTANCE;
17 }
18}
- 懒汉式 (线程安全不安全)
1/**
2 * 描述: 懒汉式(线程不安全)
3 */
4public class Singleton3 {
5
6 private static Singleton3 instance;
7
8 private Singleton3() {
9
10 }
11
12 public static Singleton3 getInstance() {
13 if (instance == null) { //这里多线程会发现线程安全性问题,可能会new()两次
14 instance = new Singleton3();
15 }
16 return instance;
17 }
18}
- 懒汉式(线程安全)(不推荐): 多线程的情况下,同一时刻只有一个线程可以进入同步方法,影响并发效率。
1package com.xdclass.couponapp.test.singleton;
2
3/**
4 * 描述: 懒汉式(线程安全)(不推荐)
5 */
6public class Singleton4 {
7
8 private static Singleton4 instance;
9
10 private Singleton4() {
11
12 }
13
14 public synchronized static Singleton4 getInstance() {
15 if (instance == null) {
16 instance = new Singleton4();
17 }
18 return instance;
19 }
20}
- 懒汉式(线程不安全)(不推荐)
1/**
2 * 描述: 懒汉式(线程不安全)(不推荐)
3 */
4public class Singleton5 {
5
6 private static Singleton5 instance;
7
8 private Singleton5() {
9
10 }
11
12 public static Singleton5 getInstance() {
13 if (instance == null) { //这里多线程会发生判断错误,new()多次
14 synchronized (Singleton5.class) {
15 instance = new Singleton5();
16 }
17 }
18 return instance;
19 }
20}
- 双重检查(推荐用): 线程安全;延迟加载;效率较高。
1# 为什么用 volatile
2* 新建对象实际上有三个步骤
3* 重排序会带来NullPonitException问题
4* 防止重排序
5* 还能防止因可见性问题造成的多次 new() 对象。
1/**
2 * 描述: 双重检查(推荐使用)
3 */
4public class Singleton6 {
5
6 //volatile 能保证变量可见性才能保证安全。 new() 对象操作不是原子性的操作。
7 private volatile static Singleton6 instance;
8
9 private Singleton6() {
10
11 }
12
13 public static Singleton6 getInstance() {
14 if (instance == null) {
15 synchronized (Singleton6.class) {
16 if (instance == null) {
17 instance = new Singleton6();
18 }
19 }
20 }
21 return instance;
22 }
23}
- 静态内部类方式 (线程安全)(可用)(懒汉式:JVM 加载时不会初始化内部类的实例的。JVM 保证是单实例)
1package com.xdclass.couponapp.test.singleton;
2
3/**
4 * 描述: 静态内部类方式,可用
5 */
6public class Singleton7 {
7
8 private Singleton7() {
9 }
10
11 private static class SingletonInstance {
12
13 private static final Singleton7 INSTANCE = new Singleton7();
14 }
15
16 public static Singleton7 getInstance() {
17 return SingletonInstance.INSTANCE;
18 }
19}
- 枚举(推荐用):单元素的枚举类型已经成为实现 Singleton 的最佳方法。(写法简单、懒加载、线程安全、避免反序列化破坏单例)
1public class User {
2 //私有化构造函数
3 private User(){ }
4
5 //定义一个静态枚举类
6 static enum SingletonEnum{
7 //创建一个枚举对象,该对象天生为单例
8 INSTANCE;
9 private User user;
10 //私有化枚举的构造函数
11 private SingletonEnum(){
12 user=new User();
13 }
14 public User getInstnce(){
15 return user;
16 }
17 }
18
19 //对外暴露一个获取User对象的静态方法
20 public static User getInstance(){
21 return SingletonEnum.INSTANCE.getInstnce();
22 }
23}
24
25public class Test {
26 public static void main(String [] args){
27 System.out.println(User.getInstance());
28 System.out.println(User.getInstance());
29 System.out.println(User.getInstance()==User.getInstance());
30 }
31}
32结果为true