文章目录
- Java内存模型
- JVM内存结构 VS Java内存模型 VS Java对象模型
- JVM内存结构
- Java对象模型
- Java内存模型(JMM)
- 为什么需要JMM(Java Memory Model)
- JMM是一种规范
- JMM是工具类和关键字的原理
- 最重要的3点内容:重排序、可见性、原子性
- 重排序
- 什么是重排序
- 重排序的好处:提高处理速度
- 重排序的3种情况
- 可见性
- 什么是可见性
- volatile关键字
- volatile是什么
- volatile适用场景与不适用场景
- volatile的两点作用
- volatile和synchronized的关系
- 用volatile可以修正重排序问题
- volatile小结
- 对synchronized可见性的正确理解
- 原子性
- 什么是原子性
- Java中的原子操作有哪些
- 原子操作 + 原子操作 != 原子操作
最近在继续加紧准备春招,加强一下Java多线程这一块,这篇文章是对于Java内存模型的一些总结,只是简单地谈谈
Java内存模型
Java实现会带来不同的“翻译”,不同CPU平台的机器指令又千差万别,无法保证并发安全的效果一致。
JVM内存结构 VS Java内存模型 VS Java对象模型
三个截然不同的概念,容易混淆
JVM内存结构
和Java虚拟机的运行时区域有关
- 组成:堆,虚拟机栈,方法区,本地方法栈,程序计数器
Java对象模型
和Java对象在虚拟机中的表现形式有关
- 是Java对象自身的存储模型
- JVM会给这个类创建一个instanceKlass,保存在方法区,用来在JVM层表示该Java类
- 当我们在Java代码中,使用new创建一个对象的时候,JVM会创建一个instanceOopDesc对象,这个对象中包含了对象头以及实例数据
Java内存模型(JMM)
为什么需要JMM(Java Memory Model)
- C语言不存在内存模型的概念
- 依赖处理器,不同处理器结果不一样,可能一个程序在不同处理器运行结果不同
- 无法保障并发安全
- 需要一个标准,让多线程运行的结果可以预期
JMM是一种规范
即这是一组规范,需要各个JVM的实现来遵循JMM规范,以便于开发者可以利用这些规范,更加方便地开发多线程程序。如没有这样的规范,那么可能经过不同JVM的不同规则的重排序之后,导致不同的虚拟机上运行的结果不一样,就会产生问题。
JMM是工具类和关键字的原理
- volatile、synchronized、Lock等的原理都是JMM
- 若没有JMM,就需要我们自己制定什么时候用内存栅栏,即什么时候同步,很麻烦
最重要的3点内容:重排序、可见性、原子性
重排序
package jmm;
import java.util.concurrent.CountDownLatch;
/**
* @Auther: Bob
* @Date: 2020/2/15 15:41
* @Description: 重排序的演示
* 直到达到某个条件才停止,用来测试小概率时间
*/
public class OutOfOrderExecution {
private static int x = 0, y = 0;
private static int a = 0, b = 0;
public static void main(String[] args) throws InterruptedException {
int i = 0;
for (; ; ) {
i++;
x = 0;
y = 0;
a = 0;
b = 0;
CountDownLatch latch = new CountDownLatch(1);
Thread one = new Thread(new Runnable() {
@Override
public void run() {
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
a = 1;
x = b;
}
});
Thread two = new Thread(new Runnable() {
@Override
public void run() {
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
b = 1;
y = a;
}
});
one.start();
two.start();
latch.countDown();
one.join();
two.join();
String result = "第" + i + "次 (" + x + "," + y + ")";
if (x == 1 && y == 1) {
System.out.println(result);
break;
} else {
System.out.println(result);
}
}
}
}
执行结果:第453次才出现了1,1
什么是重排序
在线程1内部的两行代码的实际执行顺序和代码在Java文件中的顺序不一致,代码指令并不是严格按照代码语句顺序执行的,它们的顺序被改变了,这就是重排序,这里被颠倒的是y = a 和 b = 1 这两行语句
重排序的好处:提高处理速度
重排序的3种情况
- 编译器优化:包括JVM,JIT编译器等
- CPU指令重排:就算编译器不发生重拍,CPU也可能对指令进行重拍
- 内存的“重排序”:线程A的修改线程B却看不到,引出可见性问题(不是真正的重排序)
可见性
什么是可见性
package jmm;
/**
* @Auther: Bob
* @Date: 2020/2/15 16:37
* @Description: 演示可见性带来的问题
*/
public class FieldVisibility {
volatile int a = 1;
volatile int b = 2;
private void change() {
a = 3;
b = a;
}
private void print() {
System.out.println("b = " + b + ", a = " + a);
}
public static void main(String[] args) {
while (true) {
FieldVisibility test = new FieldVisibility();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.change();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.print();
}
}).start();
}
}
}
有可见性问题的原因:
- 高速缓存的容量比主内存小,但是速度仅次于寄存器,所以在CPU和主内存之间就多了Cache层
- 线程间的对于共享变量的可见性问题不是直接由多核引起的,而是由多缓存引起的
- 如果所有核心都只用一个缓存,那么就不存在内存可见性问题了
- 每个核心都会将自己需要的数据读到独占缓存中,数据修改后也是写入到缓存中,然后等待输入到主存中。所以会导致读取的值是一个已经过期的值
“利用volatile关键字可以解决问题”
volatile关键字
volatile是什么
- voltile是一种同步机制,比synchronized或者Lock相关类更轻量,因为适用vilatile并不会发生上下文切换等开销很大的行为。
- 如果一个变量修改成volatile,那么JVM就知道了这个变量可能被并发修改
- 开销小,相应能力也小,volatile做不到synchronized那样的原子保护,volatile只在有限场景下才能发挥作用
volatile适用场景与不适用场景
- 不适用:a++
- 适用场景1:boolean flag,若一哥共享变量在程序中只是被各个线程赋值,而无其他操作,那么可以用volatile来代替synchronized或者代替原子变量,因为赋值自身具有原子性,而volatile又保证了可见性,所以足以保证线程安全。
- 适用场合2:作为刷新之前变量的触发器
volatile的两点作用
- 可见性:读一个volatile变量之前,需要先使相应的本地缓存失效,这样就必须读取到主内存读取最新值,写一个volatile属性会立即刷入到主内存。
- 禁止指令重排序优化:解决单利双重锁乱序问题
volatile和synchronized的关系
如果一个共享变量自始至终只被各个线程赋值,而没有其他的操作,那么久可以用volatile来代替synchronized或者代替原子变量,因为赋值自身是有原子性的,而volatile又保证了可见性,所以就足以保证线程安全,此时volatile可以看做轻量版的synchronized。
用volatile可以修正重排序问题
volatile小结
除了volatile可以让变量保证可见性外,synchronized、Lock、并发集合、Thread.join()和Thread.start()等都可以保证的可见性
对synchronized可见性的正确理解
- synchronized不仅保证了原子性,还保证了可见性
- synchronized不仅让被保护的代码安全,还近朱者赤(解锁之前的所有操作另一个线程都能看到)
原子性
什么是原子性
一系列的操作,要么全部执行成功,要么全部不执行,是不可分割的
Java中的原子操作有哪些
- 除了long和double之外的基本类型(int,byte,boolean,short,char,float)的赋值操作,根据Oracle的官方文档,在32位的JVM上,long和double的操作不是原子的,但是在64位的JVM上是原子的,对于64位的值的写入 ,可以分为两个32位的操作进行写入,读取错误,使用volatile解决。(在实际开发中,商用虚拟机中不会出现)
- 所有引用reference的赋值操作,不论是32位还是64位的操作系统
- java.concurrent.Atomic.*包中所有类的原子操作
原子操作 + 原子操作 != 原子操作
- 简单地把原子操作组合在一起,并不能保证整体依然具有原子性
- 全同步的HashMap也不完全安全
总结得不太好,多请见谅,多数都只是涉及概念层面的东西。。。