volatile原理及实现机制

原文出处

 

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

 

内存模型

 

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

 

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

 

多线程共享变量

 

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


如果线程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中对应的缓存行无效。

底层原理:

  1. 有volatile修饰的变量,赋值后多执行了一行“lock addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障(Memory Barrier,指重排序时不能把后面的指令重排序到内存屏障之前的位置);

  2. “lock addl $0x0, (%esp)”显然是一个空操作,关键在于lock前缀,它的作用是使得本CPU的Cache写入了内存(也就是java内存模型中的 store和write操作);该写入动作也会引起别的CPU无效化其Cache。

说明:在各个线程的工作内存中,volatile变量也可以存在不一致的情况,但由于每次使用之前都要先刷新,执行引擎看不到不一致的情况,因此可以认为不一致不存在一致性问题。但java的运算并非原子操作,导致volatile变量的运算在并发下一样不安全。

 

volatile使用场景

 

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

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

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

 

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值