Volatile关键字

195 阅读6分钟

使用场景

与锁相比,Volatile 变量是一种非常简单但同时又非常脆弱的同步机制,它在某些情况下将提供优于锁的性能和伸缩性。如果严格遵循 volatile 的使用条件即变量真正独立于其他变量和自己以前的值 ,在某些情况下可以使用 volatile 代替 synchronized 来简化代码。然而,使用 volatile 的代码往往比使用锁的代码更加容易出错。本文介绍了可以使用 volatile 代替 synchronized 的最常见的两种用例,其他的情况我们最好还是去使用synchronized。

并发编程特性

并发编程的三个特性为:原子性,可见性,有序性 Java内存模型的所以变量内型都是存在主存中,而每个线程都有自己的工作内存,线程对变量的所有操作都必须在工作内存中进行,不能直接对主存进行操作,而且线程之间是不能互相访问其他工作内存。 例如赋值操作,int i = 3,线程对变量i赋值,写入到缓存,然后再写入到主存中,而不能直接操作主存。

原子性

Java中的原子性操作是指简单的赋值读取操作,例如i=10,但是对于y=x,y++等操作不是原子性操作,因为包含读取和赋值两个原子性操作,但是对于这种复合起来的不属于原子性。 Java中提供了这类的工具包来进行原子性,java.util.concurrent.atomic包中有很多类使用了很高效的机器级指令(而不是使用锁)来保证其他操作的原子性。例如AtomicInteger类提供了方法incrementAndGet和decrementAndGet,它们分别以原子方式将一个整数自增和自减。可以安全地使用AtomicInteger类作为共享计数器而无需同步。另外这个包还包含AtomicBoolean,AtomicLong和AtomicReference这些原子类仅供开发并发工具的系统程序员使用,应用程序员不应该使用这些类。

可见性

可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果。另一个线程马上就能看到。 当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,所以对其他线程是可见的,当有其他线程需要读取时,它会去内存中读取新值。 而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。 这种在高并发环境下就是很典型的,当一个线程修改某个共享变量值,但是没来得及更新主存,而另一个线程过来读取就会发生错误情况

有序性

在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。 可以通过volatile关键字来保证一定的“有序性”。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

volatile关键字

如果volatile关键字修饰一个共享变量例如类的成员变量或者类的静态成员变量,那么这个共享变量就有两个含义

  • 这个变量对于每个线程是可见的,也就是当修改这变量值,所有线程再去读取,这个变量都会更新
  • 变量禁止指令重排序
//线程1
stop = false;
while(!stop){
  doSomething();
}

//线程2
stop = true;

如例子,采用线程2的方式来中断线程1中的循环,但是在高并发环境下面,这种方式可能带来死循环,线程之间都有自己的内存空间,在每次执行的时候,会从主存拷贝一份stop到自己的内存区域中,线程2可能在更改stop以后,没有来得及写入就转去做其他事情,这种情况就会导致线程1一直读取不到这个最新值,这种情况应该用volatile来修饰这个共享变量。 使用volatile关键字会有如下特性:

  • 使用volatile关键字会直接强制将修改值写入主存
  • 使用volatile关键字会直接使得线程1中stop值缓存无效,这样会强迫线程1直接从主存读取最新值

使用注意点

  • volatile操作不能保证操作的原子性,这里要注意
  • volatile操作在一定程度上能保存有序性,因为能禁止指令重排
  • synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下2个条件
    • 对变量的写操作不依赖于当前值 也就是不要进行自增自减操作,因为不能保证原子性
    • 该变量没有包含在具有其他变量的不变式中 例如这里例子就包含不变式
      public class NumberRange {
      private volatile int lower, upper;
      public int getLower() { return lower; }
      public int getUpper() { return upper; }
      public void setLower(int value) { 
          if (value > upper) 
              throw new IllegalArgumentException(...);
          lower = value;
        }
      public void setUpper(int value) { 
          if (value < lower) 
              throw new IllegalArgumentException(...);
          upper = value;
        }
      }
      
      如果两个线程同时到达这个地方,同时置位,就会发生一种上界下于下界的情况,这种显然是不行的

常用的两种情况

状态标志

volatile boolean shutdownRequested;
...
public void shutdown()
 { 
 shutdownRequested = true;
  }
public void doWork() { 
    while (!shutdownRequested) { 
        // do stuff
    }
}

这种需要通过一个状态标志来进行判断,一般都是循环判断的时候,能保证程序能够不陷入死循环的情况发生,使用 synchronized 块编写循环要比使用volatile 状态标志编写麻烦很多。由于volatile简化了编码,并且状态标志并不依赖于程序内任何其他状态,因此此处非常适合使用 volatile

DCL双重检查

public class Singleton {  
    private volatile static Singleton instance = null;  
    public static Singleton getInstance() {  
        if (instance == null) {  
            synchronized(this) {  
                if (instance == null) {  
                    instance = new Singleton();  
                }  
            }  
        }  
        return instance;  
    }  
}  

在这里使用volatile会或多或少的影响性能,但考虑到程序的正确性,牺牲这点性能还是值得的。DCL优点是资源利用率高,第一次执行getInstance时单例对象才被实例化,效率高。缺点是第一次加载时反应稍慢一些,在高并发环境下也有一定的缺陷,虽然发生的概率很小 在Java高级编程里面,推荐用静态内部类 所以这里最好是静态内部类来实现,能够避免DCL失效问题。

public class Singleton {
  private Singleton(){}
  public static Singleton getInstance(){
    return SingletonHolder.sInstance;
  }
  public static class SingleHolder(){
    private static final Singleton sInstance = new Singleton();
  }
}