volatile 原理

简介: volatile 原理

volatile 的底层实现原理是内存屏障,Memory Barrier(Memory Fence)

  • 对 volatile 变量的写指令后会加入写屏障
  • 对 volatile 变量的读指令前会加入读屏障

如何保证可见性

写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中

1. public void actor2(I_Result r) {
2.     num = 2;
3.     ready = true; // ready 是 volatile 赋值带写屏障
4. // 写屏障
5. }

而读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据

1. public void actor1(I_Result r) {
2. // 读屏障
3. // ready 是 volatile 读取值带读屏障
4. if(ready) {
5.         r.r1 = num + num;
6.     } else {
7.         r.r1 = 1;
8.     }
9. }

如何保证有序性

写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后

1. public void actor2(I_Result r) {
2.     num = 2;
3.     ready = true; // ready 是 volatile 赋值带写屏障
4. // 写屏障
5. }

读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前

1. public void actor1(I_Result r) {
2. // 读屏障
3. // ready 是 volatile 读取值带读屏障
4. if(ready) {
5.         r.r1 = num + num;
6.     } else {
7.         r.r1 = 1;
8.     }
9. }

还是那句话,不能解决指令交错:

  • 写屏障仅仅是保证之后的读能够读到最新的结果,但不能保证读跑到它前面去
  • 而有序性的保证也只是保证了本线程内相关代码不被重排序

以上的实现特点是:

  • 懒惰实例化
  • 首次使用 getInstance() 才使用 synchronized 加锁,后续使用时无需加锁
  • 有隐含的,但很关键的一点:第一个 if 使用了 INSTANCE 变量,是在同步块之外

但在多线程环境下,上面的代码是有问题的,getInstance 方法对应的字节码为:

1. 0: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
2. 3: ifnonnull 37
3. 6: ldc #3 // class cn/itcast/n5/Singleton
4. 8: dup
5. 9: astore_0
6. 10: monitorenter
7. 11: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
8. 14: ifnonnull 27
9. 17: new #3 // class cn/itcast/n5/Singleton
10. 20: dup
11. 21: invokespecial #4 // Method "<init>":()V
12. 24: putstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
13. 27: aload_0
14. 28: monitorexit
15. 29: goto 37
16. 32: astore_1
17. 33: aload_0
18. 34: monitorexit
19. 35: aload_1
20. 36: athrow
21. 37: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
22. 40: areturn

其中

  • 17 表示创建对象,将对象引用入栈 // new Singleton
  • 20 表示复制一份对象引用 // 引用地址
  • 21 表示利用一个对象引用,调用构造方法
  • 24 表示利用一个对象引用,赋值给 static INSTANCE

也许 jvm 会优化为:先执行 24,再执行 21。如果两个线程 t1,t2 按如下时间序列执行:

       关键在于 0: getstatic 这行代码在 monitor 控制之外,它就像之前举例中不守规则的人,可以越过 monitor 读取

INSTANCE 变量的值

       这时 t1 还未完全将构造方法执行完毕,如果在构造方法中要执行很多初始化操作,那么 t2 拿到的是将是一个未初

始化完毕的单例

       对 INSTANCE 使用 volatile 修饰即可,可以禁用指令重排,但要注意在 JDK 5 以上的版本的 volatile 才会真正有效

Synchronized关键字可以保证线程的互斥访问和可见性,但它并不能保证有序性。具体而言,即使一个线程已经执行完了synchronized代码块或方法,其内部对共享变量的修改操作也不一定立即对其他线程可见。

这是因为,在多个线程中,线程的调度是由操作系统和JVM共同决定的,并不是由程序控制的。因此,如果两个线程访问同一个资源,可能存在一种情况,即一个线程已经获取到了锁,但由于某种原因(如I/O操作、线程调度等),导致它暂停了一段时间,期间另一个线程也获得了锁并修改了共享变量,这样在第一个线程重新运行时,它的本地缓存并没有及时刷新,就会导致数据的不一致性。


相关文章
|
3月前
|
存储 算法 Sentinel
熔断降级
本内容介绍了微服务中熔断降级的实现原理及Sentinel的底层机制。通过OpenFeign集成Sentinel,利用断路器统计异常和慢请求比例,触发熔断并降级,提升系统稳定性。还讲解了Sentinel使用的限流算法,如滑动窗口、令牌桶和漏桶算法,以应对不同场景下的流量控制需求。
|
Java Spring 容器
SpringBoot中bean的生命周期
Spring Boot的Bean生命周期涉及实例化、属性注入、初始化和销毁。在实例化后,Spring通过构造函数或Setter注入属性,然后调用初始化方法(@PostConstruct、InitializingBean接口)。Bean在应用中使用后,当容器关闭时,会调用销毁方法(@PreDestroy、DisposableBean接口)。依赖注入、配置管理、组件扩展和切面编程是其常见应用场景。示例代码展示了如何通过实现BeanNameAware、BeanFactoryAware等接口以及使用@PostConstruct注解来控制Bean的初始化。
565 2
SpringBoot中bean的生命周期
|
12月前
|
XML Java 测试技术
什么是 JavaConfig?
什么是 JavaConfig?
342 7
|
安全 Java 数据库
给大忙人写的单例模式的八种实现方法
给大忙人写的单例模式的八种实现方法
167 0
|
存储 JSON Kubernetes
听GPT 讲Istio源代码--operator
听GPT 讲Istio源代码--operator
201 0
|
存储 Java
使用OutputStreamWriter写入数据
使用OutputStreamWriter写入数据
|
存储 关系型数据库 MySQL
【MySQL】存储引擎简介、存储引擎特点、存储引擎区别
【MySQL】存储引擎简介、存储引擎特点、存储引擎区别
335 2
|
存储 Java C++
JVM内存模型和结构详解(五大模型图解)
JVM内存模型和结构详解(五大模型图解)
|
设计模式 Java 数据库连接
有被问到反射的原理了,看完就学会
有被问到反射的原理了,看完就学会
186 0
|
存储 运维 监控
【Redis原理探索】帮你完全搞定Sentinel(哨兵)原理
【Redis原理探索】帮你完全搞定Sentinel(哨兵)原理
745 0
【Redis原理探索】帮你完全搞定Sentinel(哨兵)原理