1.volatile的特性
class VolatileFeaturesExample{
volatile long vl = 0L;
public void set(long l) {
vl = l;
}
public void getAndIncrement() {
vl++;
}
public long get() {
return vl;
}
这个程序在语义上和下面的程序等价:
class VolatileFeaturesExample{
volatile long vl = 0L;
public synchronized void set(long l) {
vl = l;
}
public void getAndIncrement() {
long tmp = get();
tmp += 1L;
set(tmp);
}
public synchronized long get() {
return vl;
}
关键:对一个 volatile变量的单个读 /写操作 ,与对一个普通变量的 读/写操作使用同一个锁来步 ,它们之间的执行效果相同 。
总之:
- 可见性: 对一个 volatile变量的读,总是能看到 (任意线程)对这个 volatile变量最后的写入。
- 原子性: 对任意单个volatile变量的读 /写具有原子性, 写具有原子性, 但类似于 volatile++这种复合操作不具有原子性。
2.volatile写-读建立的happens before关系
从内存语义的角度来说,volatile的写 -读与锁的释放 -获取有相同的内存效果:volatile写和锁的释放有相同内存语义; volatile读与锁的获取有相同内存语的获取有相同内存语义。
class VolatileExample{
int a = 0;
volatile boolean flag = true;
public void writer() {
a = 1; //1
flag = true; //2
}
public void reader() {
if(flag) { //3
int i = a; //4
}
}
}
假设线程A执行writer之后,线程B执行reader()方法。
- 根据程序次序规则:
1 happens-before 2
3 happens-before 4 - 根据volatile规则
2 happens-before 3
3.volatile写-读的内存语义
volatile写的内存语义:
- 当写一个volatile变量时, JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。
volatile读的内存语义:
- 当读一个 volatile变量时, JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
4.volatile内存语义的实现
JMM针对编译器制定的volatile重排序规则表:
- (写会将本地内存刷新到主内存)当第二个操作是volatile写时,不管第一个操作什么都能重排序。 这个 规则确保volatile写之前的操作不会被编译器重排序到 volatile写之后。
- (会清空本地内存,从主存读取)当第一个操作是 volatile读时,不管第二个操作是什么都能重排序。 这个 规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。
- 当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。
基于保守策略的JMM内存屏障插入策略:
- 在每个 volatile写操作的前面插入一个 StoreStore屏障
- 在每个 volatile写操作的后面插入一个StoreLoad屏障
- 在每个 volatile读操作的后面插入一个 LoadLoad屏障
- 在每个volatile读操作的后面插入一个 LoadStore屏障
StoreStore屏障保证上面所有的普通写在volatile写之前刷新到主内存。
StoreLoad屏障是避免volatile写与后面可能有的volatile读/写操作重排序。
为了保证正确实现volatile的内存语义,在每个volatile写的后面或者读前面插入一个StoreLoad屏障。从整体执行效率的角度考虑,选择写后面添加。因为volatile写-读内存语义的常见使用模式是:一个写线程写volatile变量,多个线程读同一个volatile变量。
LoadLoad屏障禁止处理器把上面的volatile读与下面的普通读重排序。LoadStore屏障禁止处理器把上面的volatile读与下面的普通写重排序。
上述 volatile写和volatile读的内存屏障插入策略非常保守。在实际执行时,只要不改变volatile写-读的内存语义,编译器可以根据具体情况省略不必要的屏障。
class VolatileBarrierExample{
int a;
volatile int v1 = 1;
volatile int v2 = 2;
void readAndWrite() {
int i = v1;
int j = v2;
a = i + j;
v1 = i + 1;
v2 = j * 2;
}
}
内存屏障的插入还可以根据具体的处理器内存模型继续优化。x86处理器除最后StoreLoad屏障外,其它的屏障都会被省略。
因为x86仅会对写-读操作做排序。这意味着x86处理器中,volatile写的开销比volatile读的开销会大很多(因为执行StoreLoad屏障开销会比较大)。
5.总结
由于 volatile仅仅保证对单个volatile变量的读 /写具有原子性, 而锁的 互斥执行特性可以确保对整个临界区代码的执行具有原子性。 在功能上,锁比 volatile更强 大;在可伸缩性和执行能上, volatile更有优势 。
网友评论