一、 volatile的应用

在多线程并发编程中synchronized和volatile都扮演着重要的角色,volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的可见性
如果volatile变量使用恰当的话,比synchronized的使用和执行成本更低,因为它不会引起线程上下文切换和调度。

1. volatile的定义与实现原理

Java允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该通过排它锁单独获得这个变量。Java提供了volatile,在某些情况下比锁更方便。
如果一个字段被声明为volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的。
CPU术语

内存屏障:是一组处理器指令,用户实现对内存操作的顺序限制。 缓存行:CPU高速缓存中可以分配的最小存储单位。

1、可见性

volatile变量在各个线程的工作内存中不存在一致性问题,但是java里面的运算并非原子操作,导致volatile变量的运算在并发下一样是不安全的

各个线程的工作内存中,volatile变量也可以存在不一致的情况,由于每次使用前都要先刷新,执行引擎看不到不一致的情况,因此可以认为不存在一致性问题

X86处理器下通过工具获取JIT编译器生成的汇编指令来查看对volatile进行写操作时,CPU会做什么事情
Java代码如下:

  1. Singletion instance = new Singletion(); //instance是volatile变量

汇编代码如下:lock addl

  1. 0x01a3deld: movb $0x0,0x1104800(%esi);
  2. 0x01a3de24: lock addl $0x0,(%esp);

有valatile修饰的共享变量进行写操作的时候会多出来第二行汇编代码,Lock前缀的指令在多核处理器下会引发了两件事
1)将当前处理器缓存行的数据写回到系统内存。
2)这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。

1.1 volatile变量运算并发不安全eg:

  1. public class VolatileTest {
  2. public static volatile int race = 0;
  3. public static final int COUNT = 20;
  4. public static void increase() {
  5. race++;
  6. }
  7. public static void main(String[] args) {
  8. Thread[] threads = new Thread[COUNT];
  9. for (int i = 0; i < COUNT; i++) {
  10. threads[i] = new Thread(new Runnable() {
  11. @Override
  12. public void run() {
  13. for (int j = 0; j < 10000; j++) {
  14. increase();
  15. }
  16. }
  17. });
  18. threads[i].start();
  19. }
  20. while (Thread.activeCount() > 1) {
  21. Thread.yield();
  22. }
  23. System.out.println(race);
  24. }
  25. }

该示例发起20个线程,每个线程对race进行1w次自增操作,如果正确并发race应该等于200000
Expect: race == 200000
Actual:race总是小于预期值
问题产生的原因是由于race++实际是由4条字节码指令构成的,自增操作不具备原子性

  1. getstatic
  2. iconst_1
  3. add
  4. putstatic
  5. 当执行任何一条指令的时候race的值都有可能被别的线程修改,当前线程还是会把当前不准确的数值赋值给race

一条字节码指令也并不意味这条指令就是原子操作。

1.2 volatile控制并发的场景举例

  1. volatile boolean shutdownRequested;
  2. public void shutdown() {
  3. shutdownRequested = true;
  4. }
  5. public void doWork() {
  6. while(!shutdownRequested){
  7. // do somthing
  8. }
  9. }

如上示例中,当shutdown()方法被调用时,能保证所有线程的doWork方法都停下来。

2、禁止指令重排优化

双重检查锁定(DCL)

双重检查锁标准代码

  1. public class safeDoubleChenkedLocking {
  2. private volatile static Instance instance; // 1
  3. public static Instance getInstance(){
  4. if( instance == null){ // 2
  5. synchronized (safeDoubleChenkedLocking.class){ // 3
  6. if( instance == null){ // 4
  7. instance = new Instance();
  8. }
  9. }
  10. }
  11. return instance;
  12. }
  13. }
  1. synchroinzed关键字加上类锁是为了防止多线程的并发问题。
  2. //2 中第一次判断为空,为了提高效率,减少加锁的成本。但是必须保证共享变量是volatile修饰的,因为如果不是volatile修饰的话会出现指令重排问题,另一个线程 执行了 为对象分配内存,instance 指向了内存。还没来得及初始化对象,就直接返回对象。
  3. 防止 线程A通过了 //2的校验,此刻线程B也通过了//2的判断,然后之后两个线程都会走一遍同步代码块,所以同步代码块内部也需要加一次判空操作。