1、案例
计算机的模型:
计算机的数据是存储于硬盘的,但是因为要计算,而硬盘的数据存取很慢,所以要把数据加载到主存中,可是因为cpu执行的还是比主存要快很多,所以执行效率还是会被主存的加载拖累。所以这里旧加上了cpu的高速缓存,一些需要执行的数据会被加入到高速缓存中。
Java内存模型和这个同理:
数据还是加载到每个线程的本地内存中执行,再交给缓存,这里就有个疑问,每个线程把这个值拿到自己加里,别人修改了该线程知道吗?很明显不知道,比如下面这个例子:
public class TestVolatile {
public static boolean initFlag = false;
public static void main(String[] args) throws InterruptedException {
new Thread(){
@Override
public void run() {
System.out.println("第一个线程准备接收消息");
while(!initFlag){
}
System.out.println("第一个线程接收到了消息");
}
}.start();
Thread.sleep(2000);
new Thread(){
@Override
public void run() {
changeFlag();
}
}.start();
}
public static void changeFlag(){
System.out.println("准备修改flag");
initFlag = true;
System.out.println("修改flag完毕");
}
}
//控制台:
//第一个线程准备接收消息
//准备修改flag
//修改flag完毕
这里可以修改:
public static volatile boolean initFlag = false;
然后就能执行出想要的效果了。
因为volatile具有能保证数据的可见性和禁止重排的特性。
2、JMM数据的原子操作
- lock(锁定):作用于主内存,它把一个变量标记为一条线程独占状态;
- read(读取):作用于主内存,它把变量值从主内存传送到线程的工作内存中,以便随后的load动作使用;
- load(载入):作用于工作内存,它把read操作的值放入工作内存中的变量副本中;
- use(使用):作用于工作内存,它把工作内存中的值传递给执行引擎,每当虚拟机遇到一个需要使用这个变量的指令时候,将会执行这个动作;
- assign(赋值):作用于工作内存,它把从执行引擎获取的值赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的指令时候,执行该操作;
- store(存储):作用于工作内存,它把工作内存中的一个变量传送给主内存中,以备随后的write操作使用;
- write(写入):作用于主内存,它把store传送值放到主内存中的变量中。
- unlock(解锁):作用于主内存,它将一个处于锁定状态的变量释放出来,释放后的变量才能够被其他线程锁定;
3、MESI解决缓存一致性问题
线程之间是不能传递参数的,那么线程2修改了值怎么让线程1拿到的呢?
黄色的是总线,当某个线程修改了变量值会马上传回主内存,当到达总线的时候,cpu总线嗅探机制一旦发现有人修改了变量值,线程会马上把自己这个线程的这个值给失效掉,一旦发现自己的值失效了,会马上去主存中取。一旦使用了volatile关键字,就会开启MESI缓存一致性协议,上面的这些步骤都会执行。
volatile的底层实现原理:
底层是通过汇编lock指令,锁定该区域的缓存,并且写回到主内存中。这一步骤对应着上面的assign步骤,底层当看到lock指令会马上把工作内存中的intFlag = true马上同步到主内存中,然后触发总线嗅探进而让其他线程的值失效。
在这里加lock也能保证线程安全的一些问题,比如经过总线的时候触发了嗅探,但是还没有write进主内存的时候,另一个线程马上感知,进而快速读取initFlag,这样读到的还是写入之前的false,或者两个线程同时写回主内存,也可能出现线程安全问题。
4、volatile不能保证原子性
public class TestAtomic {
public static volatile int num = 0;
public static void increase(){
num++;
}
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[10];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(){
@Override
public void run() {
for (int j = 0; j < 1000; j++) {
increase();
}
}
};
threads[i].start();
}
for (Thread thread:threads) {
thread.join();
}
System.out.println(num);
}
}
这样跑完计算num肯定是小于等于10000的。所以说volatile是不能保证原子性的。