Volatile关键字

Volatile这个关键字在学习的时候并未太过注意,但是前段时间,在测试自己所用的一个小项目时,意外发现其等待时间太长,当时用的是synchronized关键字。后来使用Volatile关键字后,性能明显有所改善。


Volatile 在Java是可视化的意思,字面意思来说就是被该关键字所修饰的变量能被看见。听上去类似static,但是这个可视化是对于访问该变量的各个线程来讲的。

在了解该关键字前,我们需要先看一下线程的内存模型。

当我们执行操作时,会从内存中读取或者写入数据,但是与CPU的执行速度相比,其读取和写入速度要慢很多,如果直接从内存中读取,会造成CPU执行速度下降,性能浪费。所有我们会先将需要的数据存储到CPU的高速缓存中,在这里进行数据处理,当处理完毕后,再将高速缓存的内容写入内存中。

然而这种情况就会导致一种非常著名的问题——缓存一致性问题

如果我们有两个CPU去对一个共享变量进行操作。

num = 0;
num = num + 1;

此时,线程1从内存中读取到了num的初始值为0,然后将初始值读入缓存,然后CPU1进行处理。恰巧,线程2在此时也将num的初始值0读入了缓存,而这时,线程1还未将计算结果写入到内存中。那么就会发生该问题,二者高速缓存中的结果都是num=1,写入内存后与期望不同。

这样的原因在于对于单个CPU而言,都拥有着自己的缓存区域,对于多个CPU而言,没有volatile关键字修饰的遍历在各个CPU中是无法相互可见的,当其值已经发生改变后,其他CPU并不能立刻获取到该值,而是使用自己高速缓存内已经读取到的原始值。

而解决这个问题则有两种方式

  1. 加锁synchronize
  2. 使用Volatile关键字修饰

二者虽然都能解决该问题,但是其方式有着本质性区别。

synchronize锁的方式使将该部分代码块的锁权限交个其中一个线程,当该线程访问这段内容时,其他线程进行等待,等该线程访问完毕并将数据写入内存后解锁,由其他线程竞争锁权限。这个方法是最安全的方法,能从本质上消除多线程的安全问题。通过锁的方式,实际上将多线程处理转化为单线程轮流处理。但是其弊端就是低下的性能问题。如果有一个队列要访问某个值,且队列庞大,这样等待需要花费大量时间,阻塞情况非常严重。

而volatile关键字则是将变量可视化,在一个线程完成处理后,立刻将该值分享到其他线程进行数值更新,其他线程发现这个值已经改变,将从内存中重新读取。这样的好处十分明显,所有线程能真正实现同时工作,同时对一个变量进行操控,能够大大提高效率。但是其安全性没有synchronize高。


原子性

我们提到volatile就不得不提原子性的问题,简单来说,原子性的操作就是这些操作无法中断,只能一次完成。

我们用到的赋值和读取操作都是原子性操作。举个例子

{
    int x = 1;//原子性操作,将值赋给了一个变量
    int y = x;//非原子性操作,我们先要查找x值,之后再将值赋给y,两步操作
}

在多线程中,我们需要尽量保持操作为原子性操作。非原子性操作在售票厅机制中体现的很好,会出现同一时间多个线程对共享变量进行操作,虽然有了Volatile修饰,但是还是会出现负数。

public class Test {
    public volatile int inc = 0;
 
    public void increase() {
        inc++;
    }
 
    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.increase();
                };
            }.start();
        }
 
        while(Thread.activeCount()>1)  //保证前面的线程都执行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

这里有一段代码,按照我们理想情况,执行完成后其值应该为10000.但实际上,基本上都会小于10000.

我们用了volatile关键字修饰共享变量,为什么还会出现这种情况呢?

现在我们看下这种情况的发生。

首先我们要知道,自增操作并非原子性操作,它是通过取值和增加赋值完成的。假设线程1从主存中读取到了inc的值为10,下一步要进行增加的操作,但是此时被堵塞了。此时,线程2开始读取inc的值,按照我们的期望,应该读取到的是11,但是实际上,其读取到的是inc=10,因为线程1虽然进行了读取,但并未对值进行改变时,线程就堵塞了,此时,默认对值并没有改变,系统不会判断下一步是否要改变该值,所以其他线程不可见,依旧从缓存中读取,等线程2完成操作时,inc=11.而线程1从阻塞获取执行权,执行为完成的操作,进行增长,此时读取的还是高速缓存内的inc=10。这样就造成了两次操作,实际增加1.

所有,volatile并不能保证操作的原子性,当操作非原子性时,其并没有那么优秀。


适用场景

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

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

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

实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。

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

下面列举几个Java中使用volatile的几个场景。

标记状态量
volatile boolean flag = false;
 
while(!flag){
    doSomething();
}
 
public void setFlag() {
    flag = true;
}

其操作为原子性,直接赋值即可。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值