一段代码
public class Test {
private static boolean finalInitFlag = false;
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
public void run() {
System.out.println("wait data .....");
while (!finalInitFlag){
}
System.out.println("====success");
}
}).start();
Thread.sleep(2000);
new Thread(new Runnable() {
public void run() {
System.out.println("init prepare");
finalInitFlag=true;
System.out.println("init end");
}
}).start();
}
}
执行结果会是什么呢?
wait data .....
init prepare
init end
程序会一直在init end中等待,并且不会打印====success
思考:为什么会出现这样的情况呢?我不是已经把fianlInitFlag的值改变了吗?
这要从cpu的架构说起--->>>>
cpu与内存的交互

我们知道摩尔定律当价格不变时,集成电路上可容纳的元器件的数目,约每隔18-24个月便会增加一倍,性能也将提升一倍(现在速度放缓了)。所以cpu的运行速度是非常快的,因此就会出现一个问题,内存存储的速度跟不上cpu运行的速度,如果这个问题不解决,那么cpu就算再快也是做不了很多事的。所以人们就在内存与cpu之间加上了一层高速缓存,高速缓存的速度基本可以匹配上cpu运行速度,这样就可以基本发挥出cpu运算的能力。当我们进行一次运算时,首先把数据从磁盘加载到内存中,内存再把数据加载到高速缓存中。
java的内存模型

从图中我们看出java内存模型与cpu与内存交互非常相似,工作内存可以看做是一个高速缓存,主内存可以看做就是一块内存,每个线程操作的变量其实是主内存中共享变量的副本,所以工作内存中的变量改变是不能影响其他线程的变量的
jmm定义的原子操作
- lock(锁定)
作用于主内存变量,把一个变量标识为一条线程独占的状态 - unlock(解锁)
作用于主内存变量,把一个处于锁定状态的变量释放,释放后的变量才可以被其它线程锁定
unlock之前必须将变量值同步回主内存 - read(读取)
作用于主内存变量,把一个变量的值从主内存传输到工作内存,以便随后的load - load(载入)
作用于工作内存变量,把read从主内存中得到的变量值放入工作内存的变量副本 - use(使用)
作用于工作内存变量,把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到的变量的值得字节码指令时将会执行这个操作 - assign(赋值)
作用于工作内存变量,把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作 - store(存储)
作用于工作内存变量,把工作内存中一个变量的值传送到主内存,以便随后的write操作使用 - write(写入)
作用于主内存变量,把store操作从工作内存中得到的值放入主内存的变量中
上面代码的jmm操作

具体流程为
- 代码中第一个线程对应上图的左边,线程首先把finalInitFlag=false 从主内存里面read出来,接着load进线程自己的工作内存内,最后use进执行引擎内进行运算。
- 代码中第二个线程对应上图的右边,前面流程与第一个线程一样,后面流程为执行引擎把执行结果assign进工作内存中,此时工作内存中finalInitFlag=true。接着进行store以及write操作。此时主内存的finalInitFlag=true。
看完上面的流程我们应该就能解释为什么代码走不进====success这个地方了,因为虽然主内存的变量改变了,但是线程1的工作内存没有改变,finalInitFlag一直处于false状态,所以代码一直处于死循环状态。
那么有什么办法能够让主内存变量的变化能够通知到线程呢?---->>>volatile关键字
volatile是怎么实现线程之间的通信的

在缓存与内存之间有一个底层组件叫作总线,当被volatile修饰的变量向主内存进行write操作的时候会经过总线,总线会感应到数据的变化,接着线程1会根据一个总线嗅探机制来获取到数据的变化,接着它会吧线程1内的工作内存失效掉,接着,当代码运行时发现工作内存没有该变量,则会重新的去主内存中加载对应的变量。这样就实现了线程之间的通讯问题。
上面流程其实还有一个问题就是当变量经过总线,而我们还没有对主内存进行write操作时,线程1的工作内存已经把主内存数据读取了。线程1还是只能获取到旧的数据。所以线程2在进行write之前操作时还会进行lock操作,把主内存的变量锁定,当write完后,在进行unlock操作。volatile底层原理其实是汇编语言的lock命令(有兴趣的可以加上插件,查看java运行时的汇编命令)
其次,volatile修饰的变量不会被编译器进行指令重排序
所以volatile保证了可见性,有序性两种特性,没有保证原子性(因为这些改变变量的操作不是顺序执行的)
- 原子性:原子是世界上的最小单位,具有不可分割性。比如 a=0;(a非long和double类型) 这个操作是不可分割的,那么我们说这个操作时原子操作。再比如:a++; 这个操作实际是a = a + 1;是可分割的,所以他不是一个原子操作。非原子操作都会存在线程安全问题,需要我们使用同步技术(sychronized)来让它变成一个原子操作。一个操作是原子操作,那么我们称它具有原子性。java的concurrent包下提供了一些原子类,我们可以通过阅读API来了解这些原子类的用法。比如:AtomicInteger、AtomicLong、AtomicReference等。
- 可见性:当多个线程访问同一个变量x时,线程1修改了变量x的值,线程1、线程2...线程n能够立即读取到线程1修改后的值。
- 有序性:即程序执行时按照代码书写的先后顺序执行。在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。(本文不对指令重排作介绍,但不代表它不重要,它是理解JAVA并发原理时非常重要的一个概念)。
网友评论