1.理解什么是JMM
JMM是Java内存模型,不同于JVM的。JVM是程序执行实际存在的工作空间,而JMM是java内存模型是抽象的概念,并不是真是存在,它主要是在java并发编程或者说是多线程下提出的一个描述。
JMM是围绕可见性,原子性,有序性展开
2.理解java工作内存
Snipaste_2021-04-03_22-43-48.png每个线程会有自己独有的工作内存,将主内存共享变量读取一份副本在工作内存中
执行顺序大概分成3步:1.读取变量-2.逻辑运算-3.写入主内存
按照上面3个步骤其实我们可以发现,如果实在高并发场景下,线程1对变量修改时候,还没有将修改后的变量写入主内存,那么此时线程二进来读取到旧变量就会有问题。
上代码演示
private static boolean init = false;
public static void main(String[] args) throws Exception {
new Thread( () -> {
System.out.println("线程1begin");
while (!init){
}
System.out.println("线程1end");
} ).start();
Thread.sleep(2000);
new Thread(() -> {
System.out.println("线程2begin");
init = true;
System.out.println("线程2end");
}).start();
}
代码是在main方法中开启了两个线程,线程一获取init变量,如果init为false取反后将会死循环,线程二将init变量更新为true。
思考:“线程1end”这句话会不会打印?
结论:不会,因为JMM线程模型中,提到了每个线程会有自己的内存缓存,线程1的init值是false
3.volatile关键字
被volatile修饰的关键字会让变量具有JMM内存模型中提到的可见性,可见性可以理解为线程获取变量的时候是每次都从主内存获取,修改变量的时间会马上同步到主内存,这样就保证了其他线程获取到的变量是及时可见的。
改造上面代码,将变量用volatile修饰
private volatile static boolean init = false;
public static void main(String[] args) throws Exception {
new Thread( () -> {
System.out.println("线程1begin");
while (!init){
}
System.out.println("线程1end");
} ).start();
Thread.sleep(2000);
new Thread(() -> {
System.out.println("线程2begin");
init = true;
System.out.println("线程2end");
}).start();
}
思考:“线程1end”这句话会不会打印?
结论:会打印,因为此时init变量是可见性的变量,线程1会直接从主内存中获取到init的值
4. 深入理解JMM内存模型,颠覆认知
从上面的示例中我们已经理解了volatile关键字以及线程的内存模型,但是其实没有这么简单,下面会用一下代码来做演示
private static boolean init = false;
// 注意:count使用int类型
private static int count = 0;
public static void main(String[] args) throws Exception {
new Thread( () -> {
System.out.println("线程1begin");
while (!init){
// 做一个变量自增操作
count++;
}
System.out.println("线程1end");
} ).start();
Thread.sleep(2000);
new Thread(() -> {
System.out.println("线程2begin");
init = true;
System.out.println("线程2end");
}).start();
}
思考:“线程1end”这句话会不会打印?
结论:不会
结论是不会打印,那么我们再来看看下面的代码,下面代码我把count的int类型换成Integer
private static boolean init = false;
private static Integer count = 0;
public static void main(String[] args) throws Exception {
new Thread( () -> {
System.out.println("线程1begin");
while (!init){
// 做一个变量自增操作
count++;
}
System.out.println("线程1end");
} ).start();
Thread.sleep(2000);
new Thread(() -> {
System.out.println("线程2begin");
init = true;
System.out.println("线程2end");
}).start();
}
思考:“线程1end”这句话会不会打印?
结论:会
看到这里应该很多人都不知道为啥了,会问为什么,或者不相信,下面我贴出这段代码运行后的结果在往后面说
线程1begin
线程2begin
线程2end
线程1end
很明显“线程1end”已经被打印了,看到这里其实我也不知道是为啥,因为情况很多,目前我也没找到解释这种现象的原因,很多种情况比如下面我继续试验:
private static boolean init = false;
private volatile static int count = 0;
public static void main(String[] args) throws Exception {
new Thread( () -> {
System.out.println("线程1begin");
while (!init){
// 做一个变量自增操作
count++;
}
System.out.println("线程1end");
} ).start();
Thread.sleep(2000);
new Thread(() -> {
System.out.println("线程2begin");
init = true;
System.out.println("线程2end");
}).start();
}
思考:count还是int类型,但是被我加了volatile关键字,“线程1end”这句话会不会打印?
结论:会打印
继试验了很多情况如:在while里面休眠,或者在while做一些业务逻辑操作等等,都是可以成功打印“线程1end”的
volatile及时可见性
可能现在还没办法解释上面的现象,但是我们理解的volatile关键字,是可以使我们的变量具有可见性,如果没有volatile关键字,也并不一定说不同线程就是一定在自己的副本中操作变量,永远不会从主内存中更新变量,只能说volatile关键字,为我们提供了在多线程和并发变成下,为我们的变量提供了及时的可见性,变量的可见性也是线程安全的3要素之一
线程安全需要保证3个关键点:可见性,原子性,有序性
可见性:线程直接从主内存中获取共享变量
原子性:同时成功或者同时失败
有序性:代码顺序从上往下执行,但是计算机有可能会对代码进行指令重排,会让结果出现意外情况
笔记
原子性平时我们见的比较多就不多说了,volatile可以实现可见性的同时也是可以实现有序性的,有序性还需要继续深入理解学习,这里可能没有笔记,以后会在学习的过程中慢慢补上的,学习永无止境,希望可以一直遨游在知识的海洋啦~
网友评论