目录
JMM
volatile
查看Java底层的汇编语言(了解)
有序性
双重检查锁
内存屏障
计算机多核并发缓存架构:磁盘—>主内存(RAM)—〉CPU高速缓存—>CPU寄存器
JMM: java多线程的内存模型
Java多线程内存模型与CPU缓存模型类似,是基于cpu缓存模型来建立的,Java线程内存模型是标准化的,屏蔽掉了底层不同计算机的区别。
示例代码:
package com.xxqg.mpdc.common;
/**
* @description
* @author: ZhaoYue
* @create: 2021-07-21 10:59
**/
public class TestClass {
//共享变量
private static Boolean initFlag = false;
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
System.out.println("waiting data...");
//多线程同时操作一个共享变量时,每个线程操作的都是各自共享变量的副本
//感知不到别的线程对此变量的修改
while(!initFlag){
}
System.out.println("=======================success");
}).start();
Thread.sleep(2000);
new Thread(() -> prepareData()).start();
}
public static void prepareData(){
System.out.println("prepare data...");
//每个线程操作的都是共享变量的副本
initFlag = true;
System.out.println("prepare data end");
}
}
运行结果:
想要达到预期的执行效果,解决方式:在共享变量initFlag前加volatile即可。
运行结果:
volatile:保证多线程之间共享变量的可见性。
内存模型底层八大原子操作:(JDK的产品需求)
线程修改共享变量之后,什么时候同步回主内存不一定。最迟在线程结束之前。
线程之间不能直接同步数据,需要主内存做中转。
查看Java底层的汇编语言实现需要下载一个包:查看运行代码的汇编指令.zip(笔记中的名称)
解压之后:
将这两个文件放在jdk/jre/bin目录下 。
在运行程序之前,执行以下操作:添加VM的参数以及修改刚才放入文件的JRE:
运行程序之后发现在加了volatile之后,底层汇编语言在操作共享变量时加了lock前缀
汇编代码能看懂就慢慢看吧!
指令重排序
代码示例
/**
* @description volatile测试-有序性
* @author: ZhaoYue
* @create: 2021-07-21 10:59
**/
public class TestClass {
static int x = 0 , y = 0;
static int a = 0 , b = 0;
public static void main(String[] args) throws InterruptedException {
Set resultSet = new HashSet<>();
for(int i = 0 ; i < 100000 ; i ++){
x = 0;
y = 0;
a = 0;
b = 0;
Thread one = new Thread(() -> {
//cpu会对以下两个代码做一个指令重排序
a = y; //3
x = 1; //1
});
Thread two = new Thread(() -> {
b = x; //4
y = 1; //2
});
one.start();
two.start();
one.join();
two.join();
//运行结果可能会出现 a=1 b=1可情况
resultSet.add("a=" + a + "," + "b = " + b);
System.out.println(resultSet);
}
}
}
运行结果
并发编程运行结果很容易出乎意料,导致代码出现bug,所以开发时要慎重。
并发相关知识点:并发基础(AQS、CAS、线程间通讯、ThreadLocal)、并发集合、线程池、并发工具类(闭锁)、内存模型 、锁、原子操作、线程通讯等。
as-if-serial的语义分析判断是否依赖只在单线程里,多线程管不了。
happens-before原则是说哪些情况不能重排序
/**
* @description 单例测试-阿里双重检测锁DCL对象半初始化问题
* @author: ZhaoYue
* @create: 2021-07-21 10:59
**/
public class TestClass {
// 如果不加volatile 此代码在并发场景下会有隐患
private static volatile TestClass testClass = null;
// 构造私有化
private TestClass(){}
public static TestClass getInstance(){
// 一重检测 目的是判断是否需要初始化
if(testClass == null){
// 加锁
synchronized (TestClass.class){
// 两重检测 目的是为了判断在等锁的过程中有没有被别的线程初始化
if(testClass == null){
testClass = new TestClass();
}
}
}
return testClass;
}
public static void main(String[] args) throws InterruptedException {
TestClass testClass = TestClass.getInstance();
}
}
如果想看出隐患问题需要看它的字节码文件,操作如下(需要提前安装一个可以打印方法的字节码文件的插件jclasslib)
synchronized代码块的字节码
对象的创建过程 (new)
以对象中int属性为例:先赋初值0,在执行底层的init方法赋程序员设置的默认值,最后执行构造方法赋值。
字节码中的第11行(init 赋值null)和12行(赋值new TestClass()的值),是没有依赖关系的,不违背as-if-serial语义和happens-before原则,所以说CPU是有可能对这两行代码进行指令重排序的。如果重排序了,就会先赋new TestClass()的值,此时对象就不为null了,那么别的线程就可以使用了。但这个时候还没有初始化完。即成员变量还没有赋真正的值,构造方法也还没有调用。就是对象的半初始化。解决方法是在成员变量上面加上volatile关键字。加了volatile之后就不会重排序了,原因如下:
load是读操作,store是写操作。
volatile中有lock前缀指令,相当于给这个变量前后加了内存屏障的功能,那么这个变量在赋值的过程中就不能重排序。