目录
1. 什么是 Volatile
volatile是Java提供的轻量级的同步机制,保证了可见性,不保证原子性 禁止重排序。
Java 语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量,相比于synchronized(synchronized通常称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。但是volatile 变量的同步性较差(有时它更简单并且开销更低),而且其使用也更容易出错。
volatile能解决多个cpu中高速缓存(工作内存)数据的一致性问题
2. Volatile的特性
可见性
可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的
顺序性
程序执行程序按照代码的先后顺序执行。
原子性
原子是世界上的最小单位,具有不可分割性。
3. Volatile的用法
public class Demo extends Thread {
/**
* lock 锁 汇编的指令 强制修改值,立马刷新主内存中 另外线程立马可见刷新主内存数据
*/
private static volatile boolean FLAG = true;
@Override
public void run() {
while (FLAG) {
}
}
public static void main(String[] args) throws InterruptedException {
new Demo().start();
Thread.sleep(1000);
FLAG = false;
}
}
4. CPU多核硬件架构剖析
CPU每次从主内存读取数据比较慢,而现代的CPU通常涉及多级缓存,CPU读主内存
按照空间局部性原则加载。
5. CPU的摩尔定律
https://baike.baidu.com/item/%E6%91%A9%E5%B0%94%E5%AE%9A%E5%BE%8B/350634?fr=aladdin
基本每隔18个月,可能CPU的性能会提高一倍。
6. JMM内存模型
Java内存模型定义的是一种抽象的概念,定义屏蔽java程序对不同的操作系统的内存访问差异。
6.1 主内存
存放我们共享变量的数据
6.2 工作内存
每个CPU对共享变量(主内存)的副本。
7. JMM八大同步规范
1.read(读取):作用于 主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用;从主内存读取数据
2.load(载入):作用于 工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中; 将主内存读取到的数据写入工作内存中
3.use(使用):作用于 工作内存的变量,把工作内存中的一个变量值传递给执行引擎;从工作内存读取数据来计算
4. assign(赋值):作用于 工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量;将计算好的值重新赋值到工作内存中
5.store(存储):作用于 工作内存的变量,把工作内存中的一个变量的值传送到 主内存中,以便随后的write的操作;将工作内存数据写入主内存
6.write(写入):作用于 工作内存的变量,它把store操作从工作内存中的一个变量的值传送到 主内存的变量中; 将store过去的变量值赋值给主内存中的变量
7.lock(锁定):作用于 主内存的变量,把一个变量标记为一条线程独占状态; 将主内存变量加锁,标识位线程独占状态。
8. unlock(解锁):作用于 主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定;将主内存变量解锁,解锁后其他线程可以锁定该变量
8. Volatile的底层实现原理
通过汇编lock前缀指令触发底层锁的机制
锁的机制两种:总线锁/MESI缓存一致性协议
主要帮助我们解决多个不同cpu之间缓存之间数据同步
什么是总线:
cpu和内存进行交互就得通过总线
总线锁
1.最初实现就是通过总线加锁的方式也就是上面的lock与unlock操作,但是这种方式存在很大的弊端。会将我们的并行转换为串行,从而失去了多线程的意义
2.当一个cpu(线程)访问到我们主内存中的数据时候,往总线总发出一个Lock锁的信号,其他的线程不能够对该主内存做任何操作,变为阻塞状态。该模式,存在非常大的缺陷,就是将并行的程序,变为串行,没有真正发挥出cpu多核的好处。
Intel- 64 与 IA-32架构软件开发者手册对lock指令的解释:
- 底层实现主要通过汇编lock前缀指令,它会锁定这块内存区域的缓存(缓存行锁定)并回写到主内存。
- 会将当前处理器缓存行的数据立即写回系统主内存;
- 这个写回主内存的操作会引起在其他CPU里缓存了该内存地址的数据无效(MESI协议)
8.1 Java汇编指令查看
首先需要将该工具 hsdis-amd64.dll
E:\java8\jdk\jre\bin\server 放入 hsdis-amd64.dll
-server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:CompileCommand=compileonly,*Demo01.*
注意:修改类的名称(Demo01为类的名称)
0x0000000002ae6277: lock addl $0x0,(%rsp) ;*putstatic FLAG
; - com.demo.Demo01::main@17 (line 24)
8.2 MESI协议实现的原理
总线:维护解决cpu高速缓存副本数据之间一致性问题。
- E 独享状态:在单核的情况下 ,只有一个线程 当前线程工作内存(主内存中副本数据)与主内存数据保持一致的则 当前cpu的状态:E 独享状态。
- S 表示为共享 在多个cpu的情况下 每个线程中工作内存中(主内存中副本数据)与主内存数据保持一致性,则当前cpu的状态是为:S 共享状态。
- M 修改 当前线程线程修改了工作内存中的数据,当前cpu的副本数据与主内存数据不一致性的情况下,则当前cpu的状态为M状态。
- I 无效 总线嗅探机制 如果发现cpu中副本数据与主内存数据不一致的情况下,则会认为无效需要从新刷新主内存中的数据到工作内存中。
8.3 为什么Volatile不能保证原子性
Volatile 可以保证可见性(保证每个cpu中的高速缓存数据一致性问题)
但是不能够保证数据原子性
禁止重排序
public class VolatileAtomThread extends Thread {
private static volatile int count;
public static void create() {
count++;
}
public static void main(String[] args) {
ArrayList<Thread> threads = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Thread tempThread = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
create();
}
});
threads.add(tempThread);
tempThread.start();
}
threads.forEach(thread -> {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(count);
}
}
Volatile为了能够保证数据的可见性,但是不能够保证原子性,及时的将工作内存的数据刷新主内存中,导致其他的工作内存的数据变为无效状态,其他工作内存做的count++操作等于就是无效丢失了,这是为什么我们加上Volatile count结果在小于10000以内。
8.4 为什么System.out.println 保证线程的可见性
println底层代码中使用了Synchronized, Synchronized可以保证线程可见性和线程安全性
1、线程解锁前,必须把共享变量的最新值刷新到主内存中;
2、线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量是需要从主内存中重新读取最新的值(加锁与解锁需要统一把锁)
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/131208.html