【转贴】Java中关于原子操作和volatile关键字

  • Post author:
  • Post category:java


研究ThreadPoolExecutor的时候,发现其中大量使用了

volatile

变量。不知为何,因此做了一番查找,研究: 其中借鉴了很多网上资料。 在了解

volatile

变量作用前,先需要明白一些概念:


什么是原子操作?


所谓原子操作,就是”不可中断的一个或一系列操作” , 在确认一个操作是原子的情况下,多线程环境里面,我们可以避免仅仅为保护这个操作在外围加上性能昂贵的锁,甚至借助于原子操作,我们可以实现互斥锁。 很多操作系统都为int类型提供了+-赋值的原子操作版本,比如 NT 提供了 InterlockedExchange 等API, Linux/UNIX也提供了atomic_set 等函数。


关于java中的原子性?


原子性可以应用于除long和double之外的所有基本类型之上的“简单操作”。对于读取和写入出long double之外的基本类型变量这样的操作,可以保证它们会被当作不可分(原子)的操作来操作。 因为JVM的版本和其它的问题,其它的很多操作就不好说了,比如说++操作在C++中是原子操作,但在Java中就不好说了。 另外,Java提供了AtomicInteger等原子类。再就是用原子性来控制并发比较麻烦,也容易出问题。



volatile

原理是什么?


Java中

volatile

关键字原义是“不稳定、变化”的意思

使用

volatile

和不使用

volatile

的区别在于JVM内存主存和线程工作内存的同步之上。

volatile

保证变量在线程工作内存和主存之间一致。

其实是告诉处理器, 不要将我放入工作内存, 请直接在主存操作我.

接下来是测试 :(通过测试能更好的发现和分析问题)

申明了几种整形的变量,开启100个线程同时对这些变量进行++操作,发现结果差异很大:

>>Execute End:

>>Atomic: 100000

>>VInteger: 38790

>>Integer: 68749

>>Source i: 99205

>>Source Vi: 99286

也就是说除了Atomic,其他的都是错误的。

我们通过一些疑问,来解释一下。


1:为什么会产生错误的数据?


多线程引起的,因为对于多线程同时操作一个整型变量在大并发操作的情况下无法做到同步,而Atom提供了很多针对此类线程安全问题的解决方案,因此解决了同时读写操作的问题。



2:为什么会造成同步问题?


Java多线程在对变量进行操作的时候,实际上是每个线程会单独分配一个针对i值的拷贝(独立内存区域),但是申明的i值确是在主内存区域中,当对i值修改完毕后,线程会将自己内存区域块中的i值拷贝到主内存区域中,因此有可能每个线程拿到的i值是不一样的,从而出现了同步问题。



3:为什


么使用

volatile

修饰integer变量后,还是不行?


因为

volatile

仅仅只是解决了存储的问题,即i值只是保留在了一个内存区域中,但是i++这个操作,涉及到获取i值、修改i值、存储i值(i=i+1),这里的

volatile

只是解决了存储i值得问题,至于获取和修改i值,确是没有做到同步。



4:既然不能做到同步,那为什么还要用

volatile

这种修饰符?


主要的一个原因是方便,因为只需添加一个修饰符即可,而无需做对象加锁、解锁这么麻烦的操作。但是本人不推荐使用这种机制,因为比较容易出问题(脏数据),而且也保证不了同步。



5:那到底如何解决这样的问题?


第一种:采用同步synchronized解决,这样虽然解决了问题,但是也降低了系统的性能。

第二种:采用原子性数据Atomic变量,这是从JDK1.5开始才存在的针对原子性的解决方案,这种方案也是目前比较好的解决方案了。



6:Atomic的实现基本原理?


首先Atomic中的变量是申明为了

volatile

变量的,这样就保证的变量的存储和读取是一致的,都是来自同一个内存块,然后Atomic提供了getAndIncrement方法,该方法对变量的++操作进行了封装,并提供了compareAndSet方法,来完成对单个变量的加锁和解锁操作,方法中用到了一个UnSafe的对象,现在还不知道这个UnSafe的工作原理(似乎没有公开源代码)。Atomic虽然解决了同步的问题,但是性能上面还是会有所损失,不过影响不大,网上有针对这方面的测试,大概50million的操作对比是250ms : 850ms,对于大部分的高性能应用,应该还是够的了。