本文是学习悟空老师的视频教程线程八大核心基础后所做的心得笔记,想更加具体了解其中知识的小伙伴可以前往慕课网悟空老师的课程中进行学习
Java内存模型(JMM)
介绍:是一组规范,是工具类(Lock等)和关键字(synchronized,volatile)的原理,包含最重要的三点(重排序,可见性,原子性)
为什么需要?例如C语言没有,它的处理依赖处理器,不同处理器结果不同 -> 无法保证并发安全 -> 所以我们需要一组规范(JMM),使开发者能够利用规范,更方便地开发多线程程序
如果没有:经过不同的JVM(oracle,openjdk等)的不同规则重排序之后,导致结果不同
使用同步工具和关键字需要自己指定什么时候使用内存栅栏等,十分麻烦
重排序(OutOfOrderException)
概念:实际执行顺序与代码顺序不同(发生原因:修改顺序运行的结果对当前线程没有影响/没有变化)
优点:提高处理速度(代码指令优化)
三种情况:
- 编译器优化(JVM,JIT编译器等)
- CPU优化(指令重排序)(编译器和CPU重排类似)
- 内存“重排序”(即可见性问题,线程A修改线程B看不到)(并不是真正的重排序)
例子
初始化:a=0;b=0;x=0;y=0;
package com.hbj.learning.jmm;
import java.util.concurrent.CountDownLatch;
/**
* 演示重排序现象
* 因为重排序不是每次都出现的,所以要不断重试,直到达到某个条件才停止(可以用于测试小概率时间)
*
* @author hbj
* @date 2019/11/6 21:52
*/
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(3);
Thread one = new Thread(new Runnable() {
@Override
public void run() {
try {
latch.countDown();
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
a = 1;
x = b;
}
});
Thread two = new Thread(new Runnable() {
@Override
public void run() {
try {
latch.countDown();
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
b = 1;
y = a;
}
});
one.start();
two.start();
latch.countDown();
one.join();
two.join();
System.out.println("执行的第" + i + "次");
System.out.println("x = " + x + ", y = " + y);
if (x == 0 && y == 0) {
break;
}
}
}
}
线程1 | 线程2 |
---|---|
a = 1;<br />x = b; | b = 1;<br />y = a; |
结果:
x=1;y=1;
x=1;y=0;(两个线程交替执行)
x=0;y=1;(两个线程交替执行)
x=0;y=0;(重排序(两个线程的内部代码顺序重排序并交替执行/其中一个线程代码重排序,另一个线程在里面穿插执行...),可见性(线程1先执行完,线程2没有看到,导致x=0,y=0)都有可能导致)
执行的第1815422次
x = 0, y = 0
可见性
线程1修改的内容不会被线程2所感知,便会出现可见性问题
package com.hbj.learning.jmm;
/**
* 演示可见性带来的问题
* b=3;a=1(第二个线程可能看不见或者看到一部分第一个线程的操作)
*
* @author hbj
* @date 2019/11/6 23:08
*/
public class FieldVisibility {
// volatile int a = 1;
// volatile int b = 2;
int a = 1;
int b = 2;
private void change() {
// 可能执行一句就刷到主内存
a = 3;
b = a;
}
private void print() {
System.out.println("a=" + a + ";b=" + b);
}
// 没有加volatile关键字
// a=3,b=3 第一个线程先执行,第二个线程再执行
// a=1,b=2 第二个线程先执行,第一个线程在执行
// a=3,b=2 两个线程交替运行
// a=1,b=3(线程间通讯,线程1只把b刷回了主存,没有把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();
}
}
}
可以使用volatile解决这个问题
但是还是出现了a=1,b=3(原因:打印不是原子性的操作)但是如果先打印b再打印a,由于volatile的保证,b=3前面的a=3一定会被第二个线程知道
因为打印可能回先执行打印a=1,然后切换线程执行a=3,b=a,然后再切换回来,便会出现b=3,a=1的情况
可见性问题出现的原因:(工作内存和主内存同步的原因)(这就是JMM存在的原因,通知CPU何时应该同步)

- 高速缓存的容量比主内存小,但是速度仅次于寄存器,所以在CPU和主内存之间多加了个Cache层
- 线程间对共享变量的可见性问题不是直接由多核(每个核心有一个独占缓存,他可能不会立即将修改的内容刷入主存)引起的,而是多缓存引起的(因为多个核心共用一个缓存的话就不会有这个问题)
JMM抽象:主内存和本地内存(不需要再去关心一级/二级,,,缓存)(主内存/本地内存是抽象的)

Happens-Before原则
定义:在时间上,动作A发生在动作B之前,B保证能看见A,这就是happens-before

volatile:轻量级的同步(由于happensBefore,线程被volatile修饰的变量之前的操作能够被它后面执行并读取volatile变量的线程感知)
volatile见本人另一篇文章探索volatile
原子性
- 概念:
一系列的操作,要么全部成功,要么全不成功
-
Java原子操作有哪些
- 除了long和double以外的基本类型(int,byte,boolean,short,char,float)赋值操作
- 所有引用reference的赋值操作,不管是32位还是64位机器
- java.concurrent.Atomic.*包下面所有类的原子操作
-
long,double的原子性
在32位上的JVM,不是原子性的,在64位的JVM,是原子性的
商用虚拟机中没有这个问题
可以使用volatile修饰(Java鼓励Java虚拟机尽量拆分64位的值,鼓励程序员将long,double用volatile或者用额外的同步去控制原子性)
原子+原子!=原子
网友评论