JAVA volatile 的原理与技巧

来源:互联网 发布:ubuntu设置无线网络 编辑:程序博客网 时间:2024/06/02 22:34

# volatile的原理
Java内存模型对volatile专门定义了一些特殊的访问规则。
我们用通俗的语言解释一下:


## 保持立即可见性
当一个变量定义为volatile后,若一条线程修改了这个变量的值,**新的值对于其他线程是立即可知的。**
引用《java并发编程实战》的描述:


> 当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是时时共享的,volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量的时总会返回最新的值。


## 禁止指令重排序
静止使用指令重排序优化( 重排序通常是编译器或运行时环境为了优化程序性能而采取的对指令进行重新排序执行的一种手段。)
重排序会如何干扰我们的程序呢?举例如下:(摘自《深入理解JAVA虚拟机—JVM高级特性与最佳实践》)
``` java
//此变量必须被定义为volatile  
votatile boolean initialized = false;  


//假设以下代码在线程A中执行
//线程A要完成读取配置信息的工作,当读取完成后,将initialized变量设置为true以通知其他线程配置已经可用  
processConfig(...);  
initialized = true;  


//假设以下代码在线程B中执行  
//等待initialized为true  
while(!initialized){  
  sleep();  
}  
//使用线程A中初始化好的配置信息  
doSomethingWithConfig();
```
如果定义initialized变量时,没有使用volatile修饰,就可能由于指令重排序的优化,***导致"initialized = true"这行代码被提前执行***。
这样线程B在执行使用配置信息的代码时就会出错。而使用volatile关键字则可以避免此类的情况发生。


# volatile的应用场景 
《java并发编程实战》一书中,描述了volatile变量的一种典型用法:


> 检查某个状态标记以判断是否退出循环:


``` java
volatile boolean asleep;  
.....  
    while(!asleep){  
        countSomeSheep();  
    }  
```


正如上面这个例子,**volatile变量通常用于做某个操作完成、发生中断或者状态的标志**。尽管volatile变量也可以用于表示其他的状态信息,但是在使用的时候要非常小心,例如,volatile的语义不足以确保递增操作(count++)的原子性,除非你能确保只有一个线程对变量执行写操作。
如下例:
```java
public class TestRaceCondition {    
    private volatile int i = 0;    
    public void increase() {    
       i++;    
    }    
    public int getValue() {    
       return i;    
    }    
}   
```
当多线程执行increase方法时, 是否能保证它的值会是线性递增的呢? 
答案是否定的.
 
原因:
这里的increase方法, 执行的操作是 i = i + 1;在计算中会被拆成**i+1, i=加法结果**两步
假设:**i初始为0**
```flow
st=>start: 开始
e=>end: 结束
thread1plus=>operation: A线程执行i+1,结果为1,此时还未将1赋值给i
thread2plus=>operation: B线程执行i+1,结果为1(因为A线程未及时将结果赋值给i)
thread2set=>operation: B执行i=加法结果,i=1
thread1set=>operation: A执行i=加法结果,i=1,可以发现结果错误


st->thread1plus->thread2plus->thread2set->thread1set->e
```
如果, 当前线程执行完加法运算,**但未将结果及时赋值给i**, 此时其他线程已数次将运算结果赋值给i.
则当前线程结束时, 之前的数次运算结果都将被覆盖.
即, 执行100次increase, 可能结果是 < 100.
一般来说, 这种情况需要较高的压力与并发情况下, 才会出现.
 
所以我们要记住:


> **volatile变量只能确保对象的可见性。**


当且仅当满足以下所有条件时,才应该使用volatile变量(《java并发编程实战》)。
### 对变量的写入操作不依赖变量的当前值,或者你能保证只有单个线程更新变量。
### 改变量不会与其他状态变量一起纳入不变性条件中。
### 在访问变量时不需要加锁。


附:
如果要实现上面那个例子中的递增操作,要怎么做呢?
第一:可以使用锁。
```java
public class TestRaceCondition {    
    private int i = 0;    
    public synchronized void increase() {    
       i++;    
    }    
    public synchronized int getValue() {    
       return i;    
    }    
}  
```
**加锁机制既可以保证可见性又可以确保原子性。**
但如果使用加锁机制,当多个线程竞争锁的时候,只有一个线程能最终获得锁,其他线程需要挂起等待,这个过程会增加很多线程上下文切换的操作,非常影响性能


第二:**对于以上这种情况。可以使用硬件原语(CAS), 实现非阻塞算法**。
鉴于篇幅,我将在以后的文章中,归纳总结一下CAS的相关知识。

0 0