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操作、线程调度等),导致它暂停了一段时间,期间另一个线程也获得了锁并修改了共享变量,这样在第一个线程重新运行时,它的本地缓存并没有及时刷新,就会导致数据的不一致性。