今天再看《Java并发编程实践》的时候提到了这样一句话:volatile关键字只能保证变量的可见性,无法保证变量的原子性。
当时没懂,搜了几个事例分析了一下,顺便也分析一下可以保证原子性的atom类的源码(这里以AtomInteger为例)
先看一下volatile无法保证原子性的事例:
class IntegerTestThread implements Runnable{
public void run() {
int x = 0;
while(x<1000){
atom.ai.addAndGet(1);
atom.integer++;
atom.i++;
x++;
}
atom.endThread.addAndGet(1);
}
}
public class atom {
public static AtomicInteger ai = new AtomicInteger();
public static volatile Integer integer = new Integer(0);
public static volatile int i = 0;
public static AtomicInteger endThread = new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
new atom().testAtom();
}
public void testAtom() throws InterruptedException{
for(int i=0;i<100;i++){
new Thread(new IntegerTestThread()).start();
}
for(;;){
Thread.sleep(500);
if(atom.endThread.get()==100) {
System.out.println(">>Execute End:");
System.out.println(">>Atomic: "+atom.ai);
System.out.println(">>int: "+atom.i);
System.out.println(">>Integer: "+atom.integer);
break;
}
}
}
}
最后的输出:
>>Execute End:
>>Atomic: 100000
>>int: 98055
>>Integer: 95139
我们发现volatile修饰的和没有加volatile修饰的都有问题,说明他们不是原子性的。
为什么呢?(此部分参考博客:http://blog.youkuaiyun.com/liujinwei2005/article/details/6295666)
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开始才存在的针对原子性的解决方案,这种方案也是目前比较好的解决方案了。
既然volatile无法保证原子性,那么atom类是如何保证的,我们来看看源码。
先看下AtomInteger类的变量;
最重要的一条就是value变量了,它存的是AtomInteger的值。它是volatile,说明了一件事情:他在存储的时候,是不需要在进行任何的额外的操作的。
private volatile int value;
那我们来看看获取、修改操作。
修改的源码:
public final int getAndSet(int newValue) {
for (;;) {
int current = get();
if (compareAndSet(current, newValue))
return current;
}
}
根据代码的意思,我们可以知道,先获取当前值,然后通过一个comparaAndSet()函数进行判断。然后返回修改后的值。
<pre name="code" class="html"> public final int getAndIncrement() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return current;
}
}
这里面的Unsafe类无法看到源码,自己在网上搜了搜,这个函数的意思是:
。compareAndSet这个方法多见于并发控制中,简称CAS(Compare And Swap),意思是如果valueOffset位置包含的值与expect值相同,则更新valueOffset位置的值为update,并返回true,否则不更新,返回false。
这里可以举个例子来说明compareAndSet的作用,如支持并发的计数器,在进行计数的时候,首先读取当前的值,假设值为a,对当前值 + 1得到b,但是+1操作完以后,并不能直接修改原值为b,因为在进行+1操作的过程中,可能会有其它线程已经对原值进行了修改,所以在更新之前需要判断原值是不是等于a,如果不等于a,说明有其它线程修改了,需要重新读取原值进行操作,如果等于a,说明在+1的操作过程中,没有其它线程来修改值,我们就可以放心的更新原值了。
所以次函数的总体流程:
然后看整个函数, 所有代码被放到了一个循环里面, 如果compareAndSet()执行失败,则说明 在int current = get(); 以后,其他线程对value进行了更新, 于是就循环一次,重新获取当前值,直到compareAndSet()执行成功为止。然后返回增加之后的数字!!!
小记:(摘自blog:http://blog.youkuaiyun.com/bingjing12345/article/details/8651429)
综上,getAndIncrement() 方法并不是原子操作。 只是保证了他和其他函数对 value 值得更新都是有效的。
他所利用的是基于冲突检测的乐观并发策略。 可以想象,这种乐观在线程数目非常多的情况下,失败的概率会指数型增加。