java并发编程之Volatile关键字

####并发程序概述
Android提供了很多并发用的工具,最基本的包括AsyncTask,handler-thread,但这些并发工具依然是基于Java的并发机制的,例如AsyncTask就是利用Java的线程池来安排多个线程的执行,并且由于多线程编程的复杂性,加深对java并发编程的理解有助于我们在开发中更加得心应手。

在并发过程中,有两个问题需要需要我们注意:互斥性可见性
互斥性比较容易理解,当有多个线程访问某共享变量时,为了保证程序执行的一致性和正确性,我们必须使用互斥,来保证在某一时刻只能有一个线程对共享变量进行访问。代码示例如下:

1
2
3
4
5
6
7
8
9
public class safeCounter {
private int counter = 0;
public synchronized int getCounter() {
return counter;
}
public synchronized void increaseCounter() {
counter++;
}
}

从上面代码可以看到,为了保证计数器的正常工作(当然是在多线程环境下),我们必须通过加锁来实现互斥,因为count++语句并不是原子性的,它包含读取counter值,将counter值加1和写回counter值三个操作,而加锁的目的之一就是保证这三步操作的完整性,实现整个操作的互斥,避免多个线程同时执行而引起混乱。而另一个目的就是可以保证可见性。


可见性概括来讲就是保证被线程A修改过的变量的值能够被线程B看到。更深入一些,我们需要了解Java的内存模型。

Java内存模型将内存分为主存和工作内存,主存对应着Java内存区域划分的堆内存,工作内存对应着虚拟机栈和本地方法栈,所有线程共享主存储器,但是每个线程有各自的工作内存。在java1.3之后,所有对象在主内存分配,线程不能直接使用主内存的内容,必须先将主内存的内容加载到工作内存后,而每一个线程使用的变量都是位于自己工作内存当中的副本,被修改变量的值只有在写入了主存之后,才能够被其他线程看到。

解决可见性问题一种方法就是通过上面代码示例中的加锁机制,另一种方法就是使用Volatile关键字,但是Volatile关键字在使用起来有很多我们需要注意的地方。

####Volatile关键字
在java中,当把变量声明为volatile之后,volatile变量不会被缓存在寄存器或者其他工作内存当中,任何对变量的修改都会直接同步到主存当中,从而确保了读取volatile变量值,总会得到变量的最新值

与加锁机制有所不同,Volatile关键字仅仅可以达到锁的部分功能,就是可以保证可见性,却不能保证互斥性。这就决定了Volatile关键字只有在某些情况下才可以使用,我们必须记住Volatile关键字的使用条件:

  • 对变量的写操作不依赖于当前值
  • 该变量没有包含在具有其他变量的不变式中

在上面代码的例子中,我们看到counter变量并不满足第一个条件,因此我们不能通过volatile变量来达到我们想要的目的。

除了使用条件,我们还需要记住一条volatile的规则:

  • 当线程A首先写入了volatile变量,并且线程B随后读取该变量时,在写入volatile变量之前对A可见的所有变量的值,在B读取了voliable变量后,对B也是可见的。

在java中比较常见的或者说必须使用volatile的地方,就是double checked locked问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Singleton {
private volatile static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}

上面的代码是构造单件模式的常用代码,很多人会奇怪为什么需要使用volatile变量,实际上如果代码中没有用volatile关键字,那么这段代码并不是线程安全的。

我们假设有如下的执行顺序,当线程A获得锁之后构造instance变量完成之前,线程B会进行instance==null的判断,这时会出现一种问题,就是instance引用已经获得更新(instance!=null),但是其指向的真正的对象并没有被构造完全,所以线程B可能会得到一个构造不完全的对象的引用。如果有疑问可以参考java specificationstackoverflow

其实通过上面我们就可以了解到,volatile只是一种比较弱的加锁机制,而且并不能实现互斥性,那么我们为什么需要这个关键字呢?因为完全可以利用加锁在替代volatile关键字。原因就是volatile在性能上优于加锁,因为我们不需要每次都申请锁和释放锁,所以下面的代码相对于文章一开始给出的代码,具有更高的并发性,因为多个线程可以并发的读取counter的值,而无需加锁。

1
2
3
4
5
6
7
8
9
public class safeCounter {
private volatile int counter = 0;
public int getCounter() {
return counter;
}
public synchronized void increaseCounter() {
counter++;
}
}



并发机制与java的内存模型有很大的关联,如果文章有不对的地方,或者需要讨论的地方,欢迎指出来&一起讨论。