volatile原理
在Java并发编程中synchronized和volatile扮演者重要的角色,volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的可见性和程序执行的有序性。如果volatile使用恰当的话,它比** **synchronized的使用和执行成本更低,因为它不会引起线程上下文切换和调度。 我们了解一下计算机,例如在我们工作当中大多数都是多核,由于CPU和物理主存速度不一致问题,为了解决CPU读取内存指令和数据效率问题,诞生了CPU高速缓存。
private volatile instance = new Singleton();
在生成汇编代码时会在volatile修饰的共享变量进行写操作的时候会多出Lock前缀的指令 ,Lock前缀指令在多核处理器下会发生两件事。
为了提高处理器速度,首先处理器不直接和内存进行通信,而是先将系统内存的数据读到高速缓存(L1,L2,L3)后再进行操作,但是操作何时会写到内存,如果对声明volatile的变量进行写操作,JVM 就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写到系统内存。但是就算写到内存,如果其他处理器存在的值还是旧的,再执行计算操作就会有问题。所以,在多处理器下,为了保证各处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址别修改,就会将当前处理器的缓存行设置为无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。 因此,我们得出一下结论:
这样针对volatile变量通过这样的机制就使得每个线程都能获得该变量的最新值,即可见性。
那么下面我们通过代码证明volatile如何保证可见性? 保证不同线程对这个变量进行操作时的可见性,即变量一旦改变所有线程立即可以看到
/**
* 保证可见性
*/
public class ResourceData {
// voliate 关键字能保证变量的可见性,多个线程修改同一个变量时,一个线程修改完,另一个线程获取到的是修改之后的值。
private volatile int number = 0;
public void add() {
this.number = 10;
}
public static void main(String[] args) {
ResourceData resourceData = new ResourceData();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " come in ");
try {
TimeUnit.SECONDS.sleep(3);
resourceData.add();
System.out.println(Thread.currentThread().getName() + " update number value " + resourceData.number);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "AA").start();
// 第二个线程是main
while (resourceData.number==0) {
}
System.out.println(Thread.currentThread().getName()+" mission is over ,main get number value :" +resourceData.number);
}
}
AA come in
AA update number value 10
如果共享变量不加volatile ,没有可见性,程序无法停止,加了volatile保证可见性,程序可以停止。
原理解释:
不保证原子性,代码实现:
/**
* 不保证原子性
*/
public class ResourceData2 {
// volatile 关键字不能保证变量的原子性,当多个线程修改一个变量的时候可能回出现写丢失的情况。
// 如何保证数据的原子性呢
private volatile int number = 0;
// AtomicInteger 包装类可以保证变量的原子性
AtomicInteger atomicInteger = new AtomicInteger();
public void addAtomic() {
atomicInteger.getAndIncrement();
}
public void add() {
number++;
}
public static void main(String[] args) {
ResourceData2 resourceData = new ResourceData2();
for (int i = 0; i <20 ; i++) {
new Thread(()-> {
for (int j = 0; j < 1000; j++) {
resourceData.add();
resourceData.addAtomic();
}
},String.valueOf(i)).start();
}
// 等待上面的线程全部计算完,再通过main线程获取最终的结果
while (Thread.activeCount()>2) {
// 指main 和Gc
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+" finally number value :"
+resourceData.number+" finally atomicInteger value :"+resourceData.atomicInteger);
}
}
main finally number value :19778 finally atomicInteger value :20000
预期结果是:20000,但是实际结果是:19778,那么是什么原因导致的呢?
重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段,有时候会改变程序语句的先后顺序(不存在数据依赖关系,可以重排序;存在数据依赖关系,禁止重排序) 重排序的分类和执行流程
数据依赖性:若两个操作访问同一变量,且这两个操作中有一个为写操作,此时两操作间就存在数据依赖性(存在数据依赖关系,会禁止重排序,因为会导致程序运行结果不同), 如果不存在依赖关系,可以重新排序。
如果本文对你有帮助的话,欢迎点赞,非常感谢,欢迎关注公众号:阿福聊编程
页面更新:2024-04-07
本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828
© CopyRight 2008-2024 All Rights Reserved. Powered By bs178.com 闽ICP备11008920号-3
闽公网安备35020302034844号