面试系列-volatile关键字详解

作者:海子https://www.cnblogs.com/dolphin0520/p/3920373.html

Java架构师之路做了编排和配图

volatile关键字虽然从字面上理解起来比较简单,但是要用好不是一件容易的事情。由于volatile关键字是与Java的内存模型有关的,因此在讲述volatile关键之前,我们先来了解一下与内存模型相关的概念和知识,然后分析了volatile关键字的实现原理,最后给出了几个使用volatile关键字的场景。

内存模型

程序在执行过程中,每条指令都是通过CPU来执行的,存在数据的读取和写入。程序运行过程中的临时数据存放在主内存(物理内存)中,cpu执行速度很快,要是直接从主内存中读取和写入数据,那执行指令的速度要慢很多,所以cpu要引入高速缓存也叫工作内存。

程序实际运行过程,会将运算需要的数据从主内存中复制一份到cpu高速缓存当中,直接从高速缓存中读取和写入数据,运算结束后再把高速缓存的数据刷新到主内存当中。

多线程共享变量

每个线程都有自己独立的工作内存,多个线程同时访问一个变量会存在缓存一致性问题。假如X的初始值为1,可能存在线程1和2都执行X=X+1,cpu执行时工作内存1和工作内存2中X的值都为1,两个线程执行完后刷新到主内存,最终导致执行结果为2,而不是3,。如下图:

面试系列-volatile关键字详解_第1张图片

如果线程1对变量的修改能够被线程二看到,需要做如下操作:

1.线程1把修改后的变量从工作内存1中刷新到主内存中

2.主内存把最新的变量值更新到线程2的工作内存中

并发编程

并发编程中有三个问题:原子性可见性有序性

原子性

原子性:即一个操作或者多个操作要么全部执行且执行过程中不会被打断,要么就都不执行。举例从账户A向账户B转1000元,需要先从账户A减去1000元,再往账户B加上1000元。如果没有原子性保护,从账户A减去1000元,操作突然中止。导致A账户少了1000元,而B账户没有加上1000元。

可见性

可见性:指多个线程访问同一变量时,一个线程修改了变量值,其他线程能够立即看到修改的值。

举例

//线程1执行的代码
int i = 0;
i = 10;
//线程2执行的代码
int j = i;

线程1执行完后i的值为10,线程2执行完后j的值仍为0,这就是可见性问题。

有序性

有序性指程序执行的顺序按照代码的先后顺序执行。

举例

int i = 0;
int j = 0;
i = 1;   //语句1
j = 2;   //语句2

程序在执行时不一定语句1先执行,语句2后执行,因为cpu对执行进行了重排序,因为处理器为了提高程序的运行效率,可能会对输入的代码进行优化,就不能保证执行的先后顺序和代码的先后顺序一样,但能保证执行的结果和代码顺序执行结果一致。

剖析volatile关键字

以上的讲述都是深入理解volatile关键字做铺垫,下边进入正题。

volatile关键字两层语义

一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,就具备了两层语义:

1.保证了不同线程对变量进行操作时的可见性,即一个线程修改了变量的值,修改后的值对其他线程来说是立即可见的。

2.禁止进行指令重排序。

volatile不能保证原子性

如下代码示例:

    public volatile int num = 0;
    public void increase(){
        num++;
    } 
    public static void main(String[] args) throws InterruptedException {
        VolatileTest volatileTest = new VolatileTest();
        for (int i=0; i<10; i++){
            new Thread(){
                @Override
                public void run(){
                    for (int j=0; j<1000; j++){
                        volatileTest.increase();
                    }
                }
            }.start();
        }
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(volatileTest.num);
    }
}

运行结果发现每次都不一样,我们期望的值是:10000,导致原因如下:

自增操作不是原子性的,而且volatile也无法保证对变量操作的原子性

如想保证原子性操作可以使用如下方法:

1.increase方法前加synchronized关键字。

2.increase方法中加锁。

3.使用AtomicInteger类。

volatile原理及实现机制

下面这段话摘自《深入理解Java虚拟机》:

  “观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”

lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

  1. 确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成。

  2. 它会强制将对缓存的修改操作立即写入主存。

  3. 如果是写操作,它会导致其他CPU中对应的缓存行无效。

volatile使用场景

synchronized关键字是防止多个线程同时执行一段代码,但会很影响执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下2个条件:

1.对变量的写操作不依赖于当前值.

2.该变量没有包含在具有其他变量的不变式中.

我的理解就是上面的2个条件需要保证操作是原子性操作,才能保证使用volatile关键字在并发时能够正确执行。

往期推荐

 

【技术篇】 

【技术篇】 

【生活篇】 

640?wx_fmt=jpeg

你可能感兴趣的:(面试系列-volatile关键字详解)