前言
在理解volatile前,先理解原子性、可见性、有序性
原子性:
操作的不可分割性。如++count 实际上是可以分割的三个独立操作,读取->修改->写入,其结果依赖之前的状态,所以并非原子性
可见性:
一个线程修改了对象状态后, 其他线程能够看到发生的状态变化
"可见性是一种复杂的属性,因为可见性中的错误总是会违背我们的直觉。在单线程环境中,如果向某个变量先写入值,然后在没有其他写入操作的情况下读取这个变量,那么总能得到相同的值。这看起来很自然。然而,当读操作和写操作在不同的线程中执行时,情况却并非如此,这听起来或许有些难以接受。通常,我们无法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚至是根本不可能的事情。为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制。"
java内存模型中通过Happens-Before规则来保证可见性。
有序性:
在多个线程执行指令操作时,JMM(Java内存模型)会通过指令重排序来优化操作。但在没有充分同步的程序中,如果调度器采用不恰当的方式来交替执行不同线程的操作,那么将导致不正确的结果。更糟的是,JMM还使得不同线程看到的操作执行顺序是不同的,从而导致徉缺乏同步的情况下,要推断操作的执行顺序将变得更加复杂。
public class PossibleReordering {
static int x = 0, y = 0 ;
static int a = 0, b = 0;
public static void main(String[] args) throws InterruptedException {
Thread one = new Thread(new Runnable() {
@Override
public void run() {
a = 1;
x = b;
}
});
Thread other = new Thread(new Runnable() {
@Override
public void run () {
b = 1;
y = a;
}
});
one.start ( ) ; other.start();
one.join( ) ; other.join( ) ;
System.out.println("("+x+","+y+")"); //无法正确预测x , y的值
}
}
所以我们需要通过一些方式与规则来保证多线程程序执行的有序性。
Volatile理解
1.当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的, 因此不会将该变量上的操作与其他内存操作一起重排序。
2.volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
3.volatile变量的写入操作必须在对该变量的读操作之前执行(JMM的Happens-Before规则)
实例
volatile 关键字为实例域的同步访问提供了一种免锁机制。如果声明一个域为 volatile , 那么编译器和虚拟机就知道该域是可能被另一个线程并发更新的。 例如, 假定一个对象有一个布尔标记 done , 它的值被一个线程设置却被另一个线程査询,如同我们讨论过的那样,我们可以使用volatile关键字:
private volatitle boolean done;
public boolean isDoneO { return done; }
public void setDoneO { done = true; }
局限性
volatile 变量不能提供原子性。例如方法,
public void flipDone() { done = !done; } // not atomic
不能确保翻转域中的值。不能保证读取、 翻转和写入不被中断。
使用条件
当且仅当满足以下所有条件时,才应该使甩volatile变量:
- 对变量的写人操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
- 该变量不会与其他状态变量一起纳人不变性条件中。
- 在访问变量时不需要加锁。
小结
加锁机制既可以确保可见性又可以确保原子性,而volatile变量只能保证可见性。
参考:Java并发编程实战
Java核心技术 卷I