android volatile的使用

释放双眼,带上耳机,听听看~!

今天,简单讲讲android里的volatile的使用。

这个其实很简单,而且我基本没有用到,但是还是记录一下。volatile的作用基本和sychronized相似,但是不能替代sychronized。

volatile用处说明

    在JDK1.2之前,Java的内存模型实现总是从主存(即共享内存)读取变量,是不需要进行特别的注意的。而随着JVM的成熟和优化,现在在多线程环境下volatile关键字的使用变得非常重要。

 

在当前的Java内存模型下,线程可以把变量保存在本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。

 

要解决这个问题,就需要把变量声明为volatile(也可以使用同步,参见http://blog.csdn.net/ns_code/article/details/17288243),这就指示JVM,这个变量是不稳定的,每次使用它都到主存中进行读取。一般说来,多任务环境下,各任务间共享的变量都应该加volatile修饰符。
Volatile修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。
Java语言规范中指出:为了获得最佳速度,允许线程保存共享成员变量的私有拷贝,而且只当线程进入或者离开同步代码块时才将私有拷贝与共享内存中的原始值进行比较。
这样当多个线程同时与某个对象交互时,就必须注意到要让线程及时的得到共享成员变量的变化。而volatile关键字就是提示JVM:对于这个成员变量,不能保存它的私有拷贝,而应直接与共享成员变量交互。
volatile是一种稍弱的同步机制,在访问volatile变量时不会执行加锁操作,也就不会执行线程阻塞,因此volatilei变量是一种比synchronized关键字更轻量级的同步机制。
使用建议:在两个或者更多的线程需要访问的成员变量上使用volatile。当要访问的变量已在synchronized代码块中,或者为常量时,没必要使用volatile。
由于使用volatile屏蔽掉了JVM中必要的代码优化,所以在效率上比较低,因此一定在必要时才使用此关键字

这个基本就是volatile的使用,可以修饰变量,当多线程访问时,直接访问变量的内存而不是缓存,可以使用与多线程并发。但是存在问题,下面说一下:

volatile为什么不能保证原子性?

现在我们的手机都是多核的,也就是说同时有好几颗CPU在工作,每颗CPU都有自己的Cache高速缓存,因为CPU的速度特别快,而内存的读取操作相对于CPU的运算速度来说很慢,所以就会拖累CPU的效率,引入Cache就是为了解决这个问题的,CPU先把需要的数据从内存中读到Cache中,然后直接和Cache来打交道,Cache的速度很快,因此可以保证CPU的工作效率,当Cache中的数据改变后,再将被改变的数据写回内存中。

这里写图片描述

 

首先我们分析一下多线程在访问一个普通的(没有加volatile修饰符)的变量的过程
1.CPU1和CPU2先将count变量读到Cache中,比如内存中count=1,那么现在两个CPU中Cache中的count都等于1
2.CPU1和CPU2分别开启一个线程来对count进行自增操作,每个线程都有一块自己的独立内存空间来存放count的副本。
3.线程1对副本count进行自增操作后,count=2 ; 线程2对副本count进行自增操作后,count=2
4.线程1和线程2将操作后的count写回到Cache缓存的count中
5.CPU1和CPU2将Cache中的count写回内存中的count。
那么问题就来了,线程1和线程2操作的count都是一个count副本,这两个副本的值是一样的,所以进行了两次自增后,写回内存的count=2。而正确的结果应该为count=3。这就是多线程并发所带来的问题

 

如果变量count用volatile修饰了可以解决这个问题吗?

如果一个变量加了volatile关键字,就会告诉编译器和JVM的内存模型:这个变量是对所有线程共享的、可见的,每次JVM都会读取最新写入的值并使其最新值在所有CPU可见。我们再来看一下线程在访问一个加了volatile修饰符的变量的过程

这里写图片描述

 

当count用volatile关键字修饰后,CPU1对count的值更新后,在写回内存的同时会通知CPU2 count值已经更新了,你需要从内存中获取count最新的值!

注意:这里说CPU1通知CPU2其实是不严谨的,其实这是缓存一致性机制在其作用,缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行数据时,会使缓存行无效,当CPU1将新数据写回内存后,会修改该数据在内存中的内存地址,CPU2通过嗅探在总线上传播的数据来检查自己的缓存行对应的内存地址是否被修改,如果被修改则将CPU2的该数据缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到CPU2的缓存行里。其实并不是CPU1通知CPU2,而是CPU2自己去嗅探。

其实大家只要明白了原理,怎么说也无所谓,就像好多地方都说volatile修饰的变量,线程直接和内存交互,不会保存副本。而实际上线程还是会保存副本,只不过CPU每次都会从内存中拿到最新的值,并且改变数据之后立马写回内存,看上去就像线程直接和内存交互一样。

然后CPU2中的线程如果需要使用到count的时候,就会再从内存中读取count的值来更新自己的Cache。这看上去似乎解决了我们的问题,其实问题依然存在,我们来分析一下:

比如当前count=1,CPU1和CPU2的Cache中的count都等于1,CPU1中的线程1对count进行了自增操作,然后CPU1更新了内存中count的值,并且通知CPU2 count的值已经改变,然后CPU2从内存中将count=2读到了Cache中,并且线程2开始执行count的自增操作,而就在CPU2刚刚将count的值读回Cache的时候,CPU1又更新了count的值,此时count=3,并且通知CPU2,但是此时线程2已经开始执行了,线程2已经将count=2拷贝到自己的内存空间中了,所以即使CPU2再次更新自己Cache中的count=3,也来不及了,线程2操作的是他自己内存空间中的count副本,所以线程2给count做完自增操作后,将count=3并且写回Cache,CPU2更新内存中的count。此时count的值应该是4,然而CPU2更新完count的值后仍然等于3,这样就出现了错误。我们考虑的是只有两颗CPU的情况,但是现在市面上已经有8核手机了!如果8颗CPU同时工作的话,错误会更严重!

 

Volatile一般情况下不能代替sychronized,因为volatile不能保证操作的原子性,即使只是i++,实际上也是由多个原子操作组成:read i; inc; write i,假如多个线程同时执行i++,volatile只    能保证他们操作的i是同一块内存,但依然可能出现写入脏数据的情况。如果配合Java 5增加的atomic wrapper classes,对它们的increase之类的操作就不需要sychronized。

 

总结一下,volatile可以修饰变量,用于多线程的访问,但是在多核的情况下,多线程并发还是存在问题。所以最好使用sychronized。

android volatile的使用就讲完了。

 

就这么简单。

人已赞赏
Android文章

android byte的使用

2020-4-9 0:03:06

Android文章

android ColorDrawable的使用

2020-4-9 1:18:07

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
有新消息 消息中心
搜索