Java内存模型
JVM内存结构和Java虚拟机的运行时区域有关
Java内存模型和Java并发有关
Java对象模型,Java对象在虚拟机中的表现形式,Java对象本身的存储模型
JMM(Java Memory Model)
- 是一种规范,为了让多线程运行结果可预期
- JMM是工具类和关键字的原理
- volatile, synchronized, lock的原理都是JMM
- 最重要的三个内容:重排序,可见性,原子性
为什么需要JMM:
例如C语言没有内存模型,依赖处理器的运行,不同的处理器结果不同,也就无法保证并发安全 对于Java来说,虚拟机不只一种,也就更需要一种统一的内存模型来规范了
重排序
什么是重排序
public class OutOrderExecution {
private static int a = 0, b = 0;
private static int x = 0, y = 0;
public static void main(String[] args) throws InterruptedException {
for (int i = 0; ; 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 res = "执行第" + i + "次" + "(x = " + x + ", y = " + y + ")";
if (x == 0 && y == 0) {
System.out.println(res);
break;
}else if (x == 1 && y == 1) {
System.out.println(res);
break;
} else {
System.out.println(res);
}
}
}
}
运行结果会出现四种情况 01, 10, 11,00
其中00这种情况的出现的原因: 在线程内部代码执行的顺序和实际Java程序编写代码的顺序不一样,也就是代码指令并不是严格按照代码语句顺序执行的,执行顺序被改变了,这种现象就是重排序
重排序的好处:提高处理速度,因为重排序会对指令进行优化
重排序的三种情况(这个我还不懂):1. 编译器优化 2. CPU指令重排,3. 内存的“内存的重排序”
可见性
public class FiledVisibility {
int a = 1;
volatile int b = 2;
private void change() {
a = 3;
b = a; // volatile能保证能保证读到的一定是3,也就是可以保证可见性
}
private void print() {
System.out.println("a = " + a + ", b = " + b);
}
public static void main(String[] args) {
while (true) {
FiledVisibility test = new FiledVisibility();
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();
}
}
}
1. 什么是可见性
可见性就是指当一个线程修改了共享变量的值时, 其他线程能够立即得知这个修改
JMM的抽象了主内存和本地内存
JMM为了提高读取的效率,定义了一套读写内存的规范,不需要关心一级缓存和二级缓存(CPU的处理速度远快于从内存中读取的读取速度,因此CPU和内存之间临界存储区域也就是缓存)的问题,JMM抽象出主内存和本地内存的概念
2. 主内存和本地内存的关系
-
所有变量都存储在主内存中,同时每个线程都有自己的工作内存,工作内存中的变量内容是主内存的拷贝
-
线程不能直接读取主内存中的变量,只能操作自己工作内存中的变量,然后同步到主内存中
-
主内存是多个线程共享的,但线程间不共享内存,如果线程间需要通信,必须通过主内存中转完成
总结:所有的共享变量都存在主内存中,每个线程有自己的本地内存,而对线程读写共享数据也是通过本地内存交换,(交换的过程不是实时的)所以才导致可见性问题
3. Happens-Brfore
用来解决可见性问题的,在时间上,动作A发生在动作B之前,B保证能看见A
另一种解释:如果一个操作happens-before于另一个操作,那么我们说第一个操作对于第二个操作是可见的
什么不是Happens-Brfore
两个线程没有相互配合的机制,所以代码X和Y的执行结果并不能够保证总被对方看到的,这就不具备Happens-Brfore
Happens-Brfore原则的应用
- 单线程原则
- 锁操作
- volatile 变量
- 线程启动
- 线程join
- 传递性
- 中断
- 构造方法
- 工具类的Happens-Brfore原则 ConcurrentHaspMap, get()一定能看到之前put()的操作
4.volatile关键字
volatile是什么 是一种同步机制,比synchronize或Lock相关类更加轻量,因为volatile并不会发生上下文切换等开销很大的行为。如果一个变量被修饰成volatile,那么JVM就知道这个变量可能会被并发修改。
开销小,相应的能力相对也小,虽说是用来同步保证线程安全的,但是做不到像synchronize那样的原子保护,所以使用场景有限
volatile的适用场合
- 运行不取决于之前的状态
- 刷新之前的触发器
int a = 1;
int c = 20;
volatile int b = 2;
private void change() {
a = 3;
c = 40;
b = 0; // volatile能保证能保证读到的一定是0,而且能保证a。c的值修改成之后的最新的值
}
private void print() {
if(b == 0) {
System.out.println("a = " + a + ", b = " + b + ", c = " + c);
}
}
volatile的作用:保证可见性、禁止重排序
-
可见性:读取一个volatile变量之前,需要先使相应的本地缓存失效,这样就必须到主内存中读取最新值,写一个volatile属性会立即刷入到主内存
-
禁止指令重排序的优化:解决单例模式双重锁乱序的问题
volatile和synchronize的关系:
volatile可以被看作成轻量版的synchronize:如果一个共享变量至始至终都只是被各种线程赋值,而没有其他操作,那么可以用volatile可以来代替synchronize或代替原子变量,因为赋值自身是有原子性的,而volatile又保证了可见性,所以就足以保证线程安全
用volatile可以修正重排序的问题
private static volatile int a = 0, b = 0;
private static volatile int x = 0, y = 0;
5.能保证可见性的措施
synchronize不仅保证了原子性也保证了可见性 synchronize不仅让被保护的代码安全了也让被保护代码之前的代码可见类似volatile触发器的作用
原子性
什么是原子性:
一系列的操作要么全部执行成功要么全部不执行,不会出现执行一半的情况,是不可分割的 例如a++就不是原子性的
Java中具有原子性的操作(原子操作) 除了long、double之外基本类型的赋值类型 所有引用的赋值操作
java.concurrent.Atomic.*包中所有类的原子操作
long double在32位虚拟机上不是原子的,分成两个32位进行读写,可以使用volatile解决 在64位JVM上是原子的,实际开发中虚拟机考虑带这些影响,我们就不需要关心long double出现的问题
原子操作 + 原子操作 != 原子操作