volatile关键字最详细介绍

别等时光非礼了梦想. 提交于 2019-12-15 12:45:00

目录

1 保证共享变量在多线程之间的可见性(内存可见性)

2 禁止指令重排

3 不保证原子性


1 保证共享变量在多线程之间的可见性(内存可见性)

如上图所示,所有线程的共享变量都存储在主内存中,每一个线程都有一个独有的工作内存,每个线程不直接操作在主内存中的变量,而是将主内存上变量的副本放进自己的工作内存中,只操作工作内存中的数据。当修改完毕后,再把修改后的结果放回到主内存中。每个线程都只操作自己工作内存中的变量,无法直接访问对方工作内存中的变量,线程间变量值的传递需要通过主内存来完成。

上述的Java内存模型在单线程的环境下不会出现问题,但在多线程的环境下可能会出现脏数据,例如:如果有AB两个线程同时拿到变量i,进行递增操作。A线程将变量i放到自己的工作内存中,然后做+1操作,然而此时,线程A还没有将修改后的值刷回到主内存中,而此时线程B也从主内存中拿到修改前的变量i,也进行了一遍+1的操作。最后A和B线程将各自的结果分别刷回到主内存中,看到的结果就是变量i只进行了一遍+1的操作,而实际上A和B进行了两次累加的操作,于是就出现了错误。究其原因,是因为线程B读取到了变量i的脏数据的缘故。

此时如果对变量i加上volatile关键字修饰的话,它可以保证当A线程对变量i值做了变动之后,会立即刷回到主内存中,而其它线程读取到该变量的值也作废,强迫重新从主内存中读取该变量的值,这样在任何时刻,AB线程总是会看到变量i的同一个值。

 

2 禁止指令重排

指令的执行顺序并不一定会像编写的顺序那样执行,为了保证执行上的效率,JVM可能会对指令进行重排序。比方说下面的代码

String s = "str1";
String s2 = "Str2";

上述的两条赋值语句在同一个线程之中,根据程序上的次序,“String s = "str1";的操作要先行发生于“String s2 = "str2";,但是String s2 = "str2" ;的代码完全可能会被处理器先执行。JVM会保证在单线程的情况下,重排序后的执行结果会和重排序之前的结果一致。但是在多线程的场景下就不一定了。最典型的例子就是双重检查加锁版的单例实现,代码如下所示:

public class Singleton {
 
    private volatile static Singleton instance;
 
    private Singleton() {}
 
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

由上可以看到,instance变量被volatile关键字所修饰,但是如果去掉该关键字,就不能保证该代码执行的正确性。这是因为“instance = new Singleton();”这行代码并不是原子操作,其在JVM中被分为如下三个阶段执行:

    1. 为instance分配内存
    2. 初始化instance
    3. 将instance变量指向分配的内存空间

由于JVM可能存在重排序,上述的二三步骤没有依赖的关系,可能会出现先执行第三步,后执行第二步的情况。也就是说可能会出现instance变量还没初始化完成,其他线程就已经判断了该变量值不为null,结果返回了一个没有初始化完成的半成品的情况。而加上volatile关键字修饰后,可以保证instance变量的操作不会被JVM所重排序,每个线程都是按照上述一二三的步骤顺序的执行,这样就不会出现问题。

3 不保证原子性

尽管volatile关键字可以保证内存可见性和有序性,但不能保证原子性也就是说,对volatile修饰的变量进行的操作,不保证多线程安全。请看以下的例子:

public class Test {
 
    private static CountDownLatch countDownLatch = new CountDownLatch(1000);
    private volatile static int   num            = 0;
 
    public static void main(String[] args) {
        ExecutorService executor = Executors.newCachedThreadPool();
        for (int i = 0; i < 1000; i++) {
            executor.execute(() -> {
                try {
                    num++;
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    countDownLatch.countDown();
                }
            });
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        executor.shutdown();
        System.out.println(num);
    }
}

静态变量num被volatile所修饰,并且同时开启1000个线程对其进行累加的操作,按道理来说,其结果应该为1000,但实际的情况是,每次运行结果都是一个小于1000的数字,并且不固定。那么这是为什么呢?原因是因为“num++;”这行代码并不是原子操作,尽管它被volatile所修饰了也依然如此。++操作的执行过程如下面所示:

   1. 首先获取变量i的值
   2.  将该变量的值+1
   3. 将该变量的值写回到对应的主内存中

虽然每次获取num值的时候,也就是执行上述第一步的时候,都拿到的是主内存的最新变量值,但是在进行第二步num+1的时候,可能其他线程在此期间已经对num做了多次修改,这时再进行第二三步操作之后就会覆盖了一个旧值,发生了错误。比如说:线程A在执行第一步的时候读取到此时num的值为3,然后在执行第二步之前,其他多个线程已经对该值进行了多次修改,使得num值变为了10。而线程A此时执行第二步,将原先的num值为3的结果+1变为了4,最后再将4写回到主内存中(实际此时num应该为11)。所以这也就是最后的执行结果为什么都会是一个小于1000的值的原因,内存可见性只能保证在第一步操作上的内存可见性而已。

所以如果要解决上面代码的多线程安全问题,可以采取加锁synchronized的方式,也可以使用concurrent包下的原子类AtomicInteger,以下的代码演示了使用AtomicInteger来包装num变量的方式:

public class Test {
 
    private static CountDownLatch countDownLatch = new CountDownLatch(1000);
    private static AtomicInteger  num            = new AtomicInteger();
 
    public static void main(String[] args) {
        ExecutorService executor = Executors.newCachedThreadPool();
        for (int i = 0; i < 1000; i++) {
            executor.execute(() -> {
                try {
                    num.getAndIncrement();
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    countDownLatch.countDown();
                }
            });
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        executor.shutdown();
        System.out.println(num);
    }
}

多次运行上面的代码,结果都为1000。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!