想要了解volatile之前,先去回顾一下多线程,多线程存在的意义?提高我们Http协议响应效率,能够对用户有一个比较好的体验。或者说提高程序的效率。那么怎么理解多核多线程。
多核多线程
如果是单核的CPU(默认情况下,一核两个线程)情况下,cpu在同一个时刻只能执行一个线程,所以存在切换过程,并不是真正的多线程,所以多核多线程需要的是多核cpu,少量的cpu切换
多线程的五个状态
- 创建
- 就绪
- 阻塞
- 运行
- 死亡
volatile原理分析
什么是 Volatile
能够保证线程可见性,当一个线程修改共享变量时,能够保证对另外一个线程可见性,但是注意不能够保证共享变量的原子性问题
。
Volatile的特性
- 可见性
能够保证线程可见性,当一个线程修改共享变量时,能够保证对另外一个线程可见性,但是注意他不能够保证共享变量的原子性问题。
对于可见性要怎么理解呢?
可以通过代码来看。
/**
* @author 龙小虬
* @date 2021/4/20 19:39
*/
public class Main extends Thread{
private static boolean flag = true;
@Override
public void run() {
while (flag){
}
}
public static void main(String[] args) {
new Main().start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = false;
System.out.println("主线程已经停止....");
}
}
上面这个代码在主线程中开了一个线程去运行重写的run()方法。
看看运行结果:
可以看到主线程已经停止了,但是主线程开的另一个线程却还在运行。那为什么会这样呢?这和我们平时理解的完全不同啊,flag不是被改变了吗?为什么还在运行?
其实这是因为在每个cpu中都有自己独立的存取数据的地方,而他们的数据存取是各自的高速缓存区,为什么要去使用高速缓存区?
假如不需要高速缓存区,那么我们每一次数据的存取都需要通过主内存去获取,那么会导致读取数据比较慢。就类似于我们使用mysql数据库的时候,我们会使用redis缓存或者jvm内置缓存一个道理,就是为了保证数据读取的效率问题。
上面的这个问题,我们只需要保证全局变量的可见性就能使全部线程结束,也就是在全局变量中添加volatile关键字。
- 顺序性
程序执行程序按照代码的先后顺序执行。
既然看到了线程的不可见的问题,就来了解一下JMM内存模型。
JMM八大同步规范:
- lock(锁定):作用于 主内存的变量,把一个变量标记为一条线程独占状态
- unlock(解锁):作用于 主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
- read(读取):作用于 主内存的变量,把一个变量值从主内存传输到线程的 工作内存中,以便随后的load动作使用
- load(载入):作用于 工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中
- use(使用):作用于 工作内存的变量,把工作内存中的一个变量值传递给执行引擎
- assign(赋值):作用于 工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量
- store(存储):作用于 工作内存的变量,把工作内存中的一个变量的值传送到 主内存中,以便随后的write的操作
- write(写入):作用于 工作内存的变量,它把store操作从工作内存中的一个变量的值传送到 主内存的变量中
这里面有一个总线,那他的作用是什么呢?
总线
总线:主要解决多个cpu高速缓存副本之间数据的一致性问题
实现原理:
- 总线锁的形式
效率比较低
因为只要一个线程操作主内存,那么其他线程就不允许操作主内存。当我们一个cpu线程访问到主内存数据的时候,往总线中发出一个lock锁指令,其他的cpu线程无法对我们的主内存做任何操作,这样的机制就会无法发挥多核的运行效率 - MESI形式
M
(修改)
如果当前cpu副本数据如果和主内存的数据不一致的情况下,则当前cpu状态为修改
E
(独占、互斥)
当只有一个cpu线程的情况下,cpu副本数据与主内存数据如果保存一致的情况下,则该cpu状态独享
S
(共享)
在多个cpu线程的情况下,每个cpu副本之间数据如果保持一致的情况下,则当前cpu状态为共享
I
(无效)
多个cpu副本数据不一致
既然了解了总线的实现原理,那么就来了解一下volatile实现原理,前面提到过总线是为了解决多个副本数据一致性问题,那么volatile底层就是一把lock锁,保证一致性问题,在很早的时候,是使用总线锁来实现,但是最后总线锁无法真正实现多核多线程的意义,最后使用到了MESI缓存一致性协议。这个MESI一致性协议主要就是当一个副本数据被更改,那么状态则会变为M,其他的副本数据状态变为I,那么为I的副本数据则会重新进入主内存中读取数据。
那我们怎么知道使用了lock锁呢?
我们使用一个工具来实现。
Java汇编指令查看lock指令
将hsdis-amd64.dll文件放在jdk\jre\bin\server文件夹下。注意:是jdk下的jre文件夹
在VM options:中加入-server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:CompileCommand=compileonly,*Main.*
(这里的Main为java文件名)即可打印出信息。可以看到
都是有lock锁的。