thread|Java中的volatile关键字特征和使用场景

thread|Java中的volatile关键字特征和使用场景

文章图片

thread|Java中的volatile关键字特征和使用场景

Java中的volatile关键字特征和使用场景
1.不能保证原子性首先需要注意一点 , 那就是volatile无法保证原子性 。
一个很简单的实验可以证明这点:
public class Test {    public volatile int inc = 0;    public static void main(String[
args) {        final Test test = new Test();        for(int i=0;i<10;i++){            new Thread(){                public void run() {                    for (int j = 0; j < 1000; j++) {
                       test.inc++;
                   
               ;
           .start();
               //保证前面的线程都执行完 , eclipse是1 , IDEA是2 , 因为IDEA多了个监控线程
       while (Thread.activeCount() > 2) {
           Thread.yield();
       
       System.out.println(Thread.activeCount());
       System.out.println(test.inc);
   


按理说结果是10000 , 但是真正试了就会发现 , 没几次能到10000:

可以看到 , 不仅是有些误差 , 有时候甚至差了好几千 。
这就是因为volatile没有保证原子性的缘故 。
虽然它保证了可见性和有序性 , 让每个线程都能获取最新的变量 , 但是它不能保证同一时间只有一个线程能执行增加操作 。
前面说了inc++ , 这个操作并不具有原子性 。 自增操作 , 首先是读取了inc的值(从缓存中读取 , 并非从主存中读) , 然后进行加1运算 , 再将结果存回主存 , 这是三步操作 。 可见性的保证 , 是让他在步骤2之后能立即将值存回主存 , 并使其他线程中的该变量缓存失效 。
但是没有保证原子性的情况下 , 就会有这种可能性:
①线程1从自己的工作缓存中读取了inc的值 , 为0
②线程2从自己的工作缓存中读取了inc的值 , 为0 , 然后阻塞一会
③线程1对inc进行加一运算 , 得到结果1
④线程1将结果1存回主存 , 并将其他线程中的该变量的缓存设为失效
——到这里 , 线程1已经完成了他的一次操作 , 按理说不是设为失效了吗 , 为什么线程2还是会使用错误的值 。 因为线程2已经完成了【读取】这个操作了 , 只有在它读取前 , 将变量失效 , 它才能在读取时发现异常并去主存重新获取 。
⑤线程3从自己的工作缓存中读取inc的值 , 发现已经失效 , 重新从主存获取到了变量inc的最新值1 。
——这时候线程3能取到正确的值 , 因为它还没有进行读取这个操作 。
⑥线程2对inc进行加一运算 , 得到结果1
⑦线程2将结果1存回主存 , 并将其他线程中的该变量的缓存设为失效
问题就这样产生了 , 线程2自顾自的把错误的值设置了回去 , 线程1的操作相当于白费了 。

2.volatile的使用场景volatile的效率显然是要高于synchronized的 , 因为后者会阻止其他线程访问 , 相当于强制变成单线程模式 。 但是volatile却不能完全代替synchronized , 因为它不能保证原子性 。
有时候我们可以结合两者使用 , 这样可以缩减synchronized锁住的范围 。