开发者社区> 问答> 正文

关于 volatile 关键字的应用场景

关于 volatile 关键字的应用场景我先说说我的理解,如有不对请指正,volatile 有两个作用:
<1>防止编译器优化语句,例如:

int i = 0;
int i = 1;
int i = 2;
可能会被编译器优化成 int i = 2;
<2>保证 volatile 的可见性,这个我不是特别理解。看了一下一般使用 volatile 的场景有几种:

<2.1>使用 volatile 修饰一个线程上的变量,这种情况没有意义。

class VolatileTask2 implements Runnable
{
    /*
     * 在这种情况下,使用 volatile 没有什么意义,因为对于每一个 VolatileTask2 的实例都有自己的 stop 的拷贝。
     * 每一个 stop 之间是相互独立的。
     */
    volatile boolean stop = false;
    public void run()
    {
        while(!stop){}
        System.out.println(Thread.currentThread() + " stopped!");
    }
} 
public class VolatileTest2 {
    public static void main(String[] args) {
        ExecutorService exec = Executors.newCachedThreadPool();
        for(int i = 0; i < 5; i++)
            exec.execute(new VolatileTask2());
        exec.shutdown();
    }
}

<2.2> 使用 volatile 修饰一个对象上的变量,有许多的线程可能会操作该变量,这种情况也没有什么意义。

class VolatileHolder1
{
    volatile int i = 0;
}
class VolatileTask1 implements Runnable
{
    VolatileHolder1 vh;
    public VolatileTask1(VolatileHolder1 vh){this.vh = vh;}
    public void run()
    {
        /*
         * ***这种情况下,volatile 无法保证线程安全,它只是保证了每次读取 i 的时候都是从主内存而不是线程栈上读取。***
         * 在线程运行时,线程已经将变量复制到线程栈中,为什么输出的值还是有可能正确呢(不正确的结果是由于 vh.i++ 不是原子操作引起)。
         * 因为,我们在复制的时候,只是将 VolatileTask1 的一个实例复制到了线程栈,
         * 而这个实例持有一个 VolatileHolder1 vh 的引用,vh 引用的实际位置仍然是指向堆中内存。
         */
        vh.i++;
    }
} 
public class VolatileTest1 {
    public static void main(String[] args) {
        ExecutorService exec = Executors.newCachedThreadPool();
        VolatileHolder1 vh = new VolatileHolder1();
        for(int i = 0; i < 10; i++)
            exec.execute(new VolatileTask1(vh));
        exec.shutdown();
    }
}
//这种情况下使用 volatile 没有任何的意义,因为它不能保证线程安全。
//1 4 3 2 1 5 6 7 8 9 

<2.3>使用 volatile 修饰一个线程上的一个变量,但是有可能会有另外一个线程来更改这个值。这种情况下使用 volatile
才有意义。

package com.ch18.io;

/**
 * 当我们不使用 volatile 关键字的时候:
 * 在线程进入 run() 方法的时候,我们将它复制到线程栈中,与前面的不同的是,前面复制到线程栈的是一个指向堆内存的引用。
 * 在将 stop 复制到线程栈之后,我们操作 stop 就是在线程栈上进行了。
 * 而我们在 main() 方法中,修改的并不是 run() 方法线程栈上的 stop 的值,而在 run() 方法中读取的仍然是线程栈上的 stop。
 * 
 * 当我们使用 volatile 关键字的时候,run() 方法和 stopMe() 方法都是读取主内存中的  stop 的值,所以可以正常运行。
 */
class StopTester implements Runnable {
      private volatile boolean stop = false;
//      private boolean stop = false;
      public void stopMe() {
        stop = true;
      }
      @Override
      public void run() {
          System.out.println("Thread start.");
        while(!stop) {}
        System.out.println("Thread stopped.");
      }
    }

    public class TestVolatile {
      public static void main(String[] args) throws Exception {
        StopTester tester = new StopTester();
        Thread thread = new Thread(tester);
        thread.start();
        Thread.sleep(1000);
        System.out.println("Try stop...");
        tester.stopMe();
        Thread.sleep(1000);
      }
    }

也就是说,使用 volatile 的情况是这样的:在线程 A 上有一个变量 V,这个变量 V 在线程开始的时候被复制到线程栈中。而我们在线程 B 中有可能修改这个变量(不同于 <2.2>,在 <2.2> 中被复制到线程栈上的只是一个指向堆内存的指针,而我们并没有修改这个指针。我们修改的是这个指针指向的对内存中的值。),为了保证我们修改后的变量 V 能够被线程 A 看到(不加 volatile 的话,可能线程 B 修改的是 B 的线程栈中的变量 V,而线程 A 读取的是 A 的线程栈中的变量 V),就需要加上 volatile,强制A,B两个线程都在主内存上读取、写入变量 V。

展开
收起
蛮大人123 2016-03-05 17:00:31 2719 0
1 条回答
写回答
取消 提交回答
  • 我说我不帅他们就打我,还说我虚伪

    volatile对指令重排的保证,才确保了它的可见性。题主列举的2.1,2.2两个场景并不是volatile适用的场景。
    关于volatile的理解,可以参考:http://ifeve.com/syn-jmm-volatile/

    2019-07-17 18:53:49
    赞同 展开评论 打赏
问答分类:
问答地址:
问答排行榜
最热
最新

相关电子书

更多
低代码开发师(初级)实战教程 立即下载
冬季实战营第三期:MySQL数据库进阶实战 立即下载
阿里巴巴DevOps 最佳实践手册 立即下载