先来看一篇详细分析volatile语义的文章。
http://www.infoq.com/cn/articles/java-memory-model-4
若问及volatile关键字的含义,一般都会得到如下答案。
(1)对volatile修饰的变量,各线程间的写操作将立即对其他线程可见。
(2)volatile修饰的变量的操作是原子性的,并且如果volatile的赋值操作依赖于他的前一个值,则会失去原子性。
其实,(1)有着更深层次的含义,即对可见性的理解。(2)volatile本身和原子性并无直接关系,(2)中所说的原子性其实并不是真正意义上的原子性,其实是可见性的另一种表现形式。
那何为可见性问题呢,对于同一段代码,执行线程和观察线程所见到的执行顺序不一定是一致的。听起来有点拗口,看如下例子。
乱序执行导致的可见性问题
以下文伪代码为例,解释了何谓乱序执行导致的可见性问题。
线程1:
Resource resource=init();
boolean hasInit=true;
afterOperation();
线程2:
if(hasInit){
getResource();
more operation;
}
上述程序粗看没问题,但当线程2中hasInit=true时,resource一定初始化好了吗。答案是否定的。
JAVA为了执行效率,代码在执行时候会有一定程度上的指令乱序。但对同一个线程内的程序来说,乱
序执行不影响每一句代码对之前执行的代码的可见性。对上述程序来说,当线程1中的afterOperation()获取到flag值为true,resource一定是初始化完成了的。 但对线程2来说,resource有可能未初始化完成时,就获取了为true的flag值。
回到之前volatile的语义,如下:
- 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存。
- 当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
简单来说,当上述的flag变量设置为volatile时,对线程2来说,resource的初始化一定是在volatile的写之前执行。即禁止flag的写语句重排到定义的位置之前。
那对线程2来说,线程1中的afterOperation()被线程2观察到时,是否flag一定已经是true了呢?答案是否定的。具体原因么,可参考volatile的内存含义的第二条。即需要volatile的读来禁止这种情况。
以上便是文章开头所说的 “可见性”的真正所指。另外,乱序执行是对编译后的命令来说的,一句JAVA命令被编译后的执行命令,也可以被乱序执行。这在下文提到的单例模型中会讲到。
顺便插一句,如何快速地判断多线程情况下的代码可见性呢,实际应用中可参考happens-before原则
以上其实便是可见性问题。实际应用中,除了可见性问题,更常见的便是原子性问题
原子性操作被破坏
先从最简单的变量自增操作说起。众所周知,JAVA的自增操作不是原子性的。一句JAVA命令,会被编译器分解为多条命令执行。以自增操作为例 , a++ 执行时的操作时的字节码如下
public void getNext(); Code: 0: aload_0 //加载局部变量表index为0的变量,在这里是this
1: dup //将当前栈顶的对象引用复制一份
2: getfield #2; //Field id:I,获取id的值,并将其值压入栈顶
5: iconst_1 //将int型的值1压入栈顶
6: iadd //将栈顶两个int类型的元素相加,并将其值压入栈顶
7: putfield #2; //Field id:I,将栈顶的值赋值给id
10: return }
若在执行自增操作的时候,同时有其他线程对同一变量a进行写操作或者读操作,则操作的对象可能是执行过程中状态异常的变量,并导致获取期望之外的结果。 对于具体的自增操作的多线程测试,网上很多。
原子性的概念,简单来说,就是进行一些列操作的时候,操作中的动作要么不执行,要么全部执行完后才让执行结果对其他线程可见。
上述概念一样可以运用于多条语句,举例来说,在一个方法中需要执行如下write操作。
boolean flag=true;
Date date=new Date();
这两个变量的状态必须一起更新,若当flag=true执行完毕后就立刻被其他线程读取,则其他线程中可能会获得错误的date变量。即上文说的“执行过程中状态异常的变量”。
解决方式,便是上述write操作和read操作用同一个锁进行synchronized。这样可以保证,在write操作时,不正确的值不会被其他线程读取。
原子性问题有时会和可见性问题同时出现,典型代表便是著名的基于双加锁的单例模式。
private Resource resource;
public static Resource getInstance(){
if( resource == null ){ //(1)
synchronized(lock){
if( resource == null ){
resource = new Resource(); //(4)
}
}
}
该写法中的resource变量为竞争资源,其中标注为4的语句可能会引起失败。原因就在于resource=new Resouce()这句话至少包含2个步骤,一是是调用resource对象的构造函数并初始化,二是将新分配的Resource对象的内存引用赋值给resource变量。 当另一个线程中执行语句1时,由于并未将resource设置为volatile,则可能导致获取到不为空但尚未初始化完成的resource对象。这就是上文所说的乱序执行导致的问题。
总结:
并发问题主要分为资源竞争时原子操作的被破坏、和多线程情况下乱序执行导致的线程可见性。
凡是在并发过程中遇到上述两种问题,都需要考虑解决方式。
常见的解决方式是通过加锁来实现。锁的获取也具有volatile读的内存语义,锁的释放具有volatile写的内存语义。如果功力不够,在多线程编程时则少用volatile变量来解决并发问题。用锁则稳妥些(效率牺牲)。