今天在网上看到一篇文章,谈论的是根据volatile特性来用1000个线程不断的累加数字,每次累加1个,到最后值确不是1000.
文章是有点误解别人的意思,但是在文章的评论里面,作者也指出了错误。
我根据文章的错误之处和网友的评论,总结了自己的一些方法和思路。希望跟大家探讨。
文章出处:http://www.cnblogs.com/aigongsi/archive/2012/04/01/2429166.html
此文的问题是:1000个线程可能还有N个(例如50个)线程没有执行完,主线程(main方法)就已经执行了,所以造成了最后的count值不是我们想要的值。就算等1000个线程执行完以后,再执行主线程的获取count值,数据也不一定正确。因为volatile不能保证原子性,只能保证可见性。见下面分析。
关于volatile:
java语言规范描述:每一个变量都有一个主内存。为了保证最佳性能,JVM允许线程从主内存拷贝一份私有拷贝,然后在线程读取变量的时候从主内存里面读,退出的时候,将修改的值同步到主内存。
根据上面提供的文章,用volatile好像能解决1000次累加的计算值。但是结果不是的。
首先说两个概念:原子性和可见性
原子性,根据我个人的理解:当前变量只允许一个线程来操作,不接受多线程来访问。所以每次的都是最新的值。
可见性,根据我个人的理解:变量t。A线程对t变量修改的值,对B线程是可见的。但是A获取到t的值加1之后,突然挂起了,B获取到的值还是最新的值,volatile能保证B能获取到的t是最新的值,因为A的t+1并没有写到主内存里面去。这个逻辑是没有问题的。
回到上面的1000次累加的问题,变量count,1000次累加,1000个线程,volatile能保证的是每个线程读取的变量的值在内存里面是最新的,这个没问题。
这1000个线程里面,会有这样的场景:
当第523个线程读取的count值,假设这个值为522,线程把count加1后,count为523了,但是这个时候count值还没有写入到主内存里面去,CPU在某种情况把第523个线程中止(挂起)了,这样,第524个线程从主内存读取的值还是522,当第524个线程把值写入到主内存后,count值为523,然后第523个线程开始执行(这个时候第523个线程已经加好了count的值且值为523,只是没有同步到主内存),把count值同步到主内存,这个时候,count的值还是523,第524个线程累加的值等于没有累加。
所以造成了最后数据一定不是1000。
下面的代码是我个人的理解:
import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; public class Counter { public static AtomicInteger count = new AtomicInteger();//原子操作 public static CountDownLatch latch= new CountDownLatch(1000);//线程协作处理 public static volatile int countNum = 0;//volatile 只能保证可见性,不能保证原子性 public static int synNum = 0;//同步处理计算 public static void inc() { try { Thread.sleep(1); } catch (InterruptedException e) { } countNum++; int c = count.addAndGet(1); add(); System.out.println(Thread.currentThread().getName() + "------>" + c); } public static synchronized void add(){ synNum++; } public static void main(String[] args) { //同时启动1000个线程,去进行i++计算,看看实际结果 for (int i = 0; i < 1000; i++) { new Thread(new Runnable() { @Override public void run() { Counter.inc(); latch.countDown(); } },"thread" + i).start(); } try { latch.await(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()); System.out.println("运行结果:Counter.count=" + count.get() + ",,," + countNum + ",,," + synNum); } }
count.get()是AtomicInteger的值;
count是用volatile修饰的变量的值;
synNum是用synchronized修饰的值;
所以,用synchronized和AtomicInteger能保证是你想要的数据,volatile并不能保证。
第一次运行结果:
main
运行结果:Counter.count=1000,,,991,,,1000
第二次运行结果:
main
运行结果:Counter.count=1000,,,998,,,1000
第三次运行结果:
main
运行结果:Counter.count=1000,,,993,,,1000
可见,就算用了volatile,也不能保证数据是你想要的数据,volatile只能保证你数据的可见性(获取到的是最新的数据,不能保证原子性,说白了,volatile跟原子性没关系)
要保证原子性,对数据的累加,可以用AtomicInteger类;
也可以用synchronized来保证数据的一致性。
欢迎发表不同意见和看法,共同探讨和交流。