sky ao IT哈哈
这是一个真实案例,曾经惹出硕大风波,故事的原由却很简单,就是须要实现一个简单的计数器,每次取值而后加1,因而就有了下面这段代码:java
private int counter = 0; public int getCount ( ) { return counter++; }
这个计数器被用于生成一个sessionId,这个sessionID用于和外部计费系统交互,这个sessionId理所固然的要求保证全局惟一而不重复。可是很遗憾,上面的代码最终被发现会产生相同的id,所以会形成一些请求莫名其妙的报错.....更痛苦的是,上面这段代码是一个来自其余部门开发的工具类,咱们当时只是拿了它的jar包来调用,没有源码,更没有想这里面会有如此低级而可怕的错误。安全
因为重复的sessionId,形成有个别请求失败,虽然出现几率极低,常常跑一天测试都不见得能重现一次。由于是和计费相关,所以哪怕是再低的几率出错,也不得不要求解决。实际状况是,项目开发到最后阶段,都开始作发布前最后的稳定性测试了,在7*24小时的连续测试中,这个问题每每在测试开始几天后才重现,将当时负责trouble shooting的同事折腾的很惨......通过反复的查找,终于有人怀疑到这里,反编译了那个jar包,才看到上面这段出问题的代码。session
这个低级的错误,源于一个java的基本知识:
++操做,不管是i++仍是++i,都不是原子操做! 多线程
而一个非原子操做,在多线程并发下会有线程安全的问题:这里稍微解释一下,上面的"++"操做符,从原理上讲它其实包含如下:计算加1以后的新值,而后将这个新值赋值给原变量,返回原值。相似于下面的代码并发
private int counter = 0; public int getCount ( ) { int result = counter; int newValue = counter + 1; // 1. 计算新值 counter = newValue; // 2. 将新值赋值给原变量 return result; }
多线程并发时,若是两个线程同时调用getCount()方法,则他们可能获得相同的counter值。为了保证安全,一个最简单的方法就是在getCount()方法上作同步:ide
private int counter = 0; public synchronized int getCount ( ) { return counter++; }
这样就能够避免因++操做符的非原子性而形成的并发危险。
咱们在这个案例基础上稍微再扩展一下,若是这里的操做是原子操做,就能够不用同步而安全的并发访问吗?咱们将这个代码稍做修改:工具
private int something = 0; public int getSomething ( ) { return something; } public void setSomething (int something) { this.something = something; }
假设有多线程同时并发访问getSomething()和setSomething()方法,那么当一个线程经过调用setSomething()方法设置一个新的值时,其余调用getSomething()的方法是否是当即能够读到这个新值呢?这里的"this.something = something;" 是一个对int 类型的赋值,按照java 语言规范,对int的赋值是原子操做,这里不存在上面案例中的非原子操做的隐患。性能
可是这里仍是有一个重要问题,称为"内存可见性"。这里涉及到java内存模型的一系列知识,限于篇幅,不详尽讲述,不清楚这些知识点的能够本身翻翻资料,最简单的办法就是google一下这两个关键词"java 内存模型", "java 内存可见性"。或者,能够参考这个帖子"java线程安全总结",测试
解决这里的"内存可见性"问题的方式有两个,一个是继续使用 synchronized 关键字,代码以下this
private int something = 0; public synchronized int getSomething ( ) { return something; } public synchronized void setSomething (int something) { this.something = something; }
另外一个是使用volatile 关键字,
private volatile int something = 0; public int getSomething ( ) { return something; } public void setSomething (int something) { this.something = something; }
使用volatile 关键字的方案,在性能上要好不少,由于volatile是一个轻量级的同步,只能保证多线程的内存可见性,不能保证多线程的执行有序性。所以开销远比synchronized要小。
让咱们再回到开始的案例,由于咱们采用直接在 getCount() 方法前加synchronized 的修改方式,所以不只仅避免了非原子性操做带来的多线程的执行有序性问题,也"顺带"解决了内存可见性问题。
OK,如今能够继续了,前面讲到能够经过在 getCount() 方法前加synchronized 的方式来解决问题,可是其实还有更方便的方式,可使用jdk 5.0以后引入的concurrent包中提供的原子类,java.util.concurrent.atomic.Atomic***,如AtomicInteger,AtomicLong等。
private AtomicInteger counter = new AtomicInteger(0); public int getCount ( ) { return counter.incrementAndGet(); }
Atomic类不只仅提供了对数据操做的线程安全保证,并且提供了一系列的语义清晰的方法如incrementAndGet(),getAndIncrement,addAndGet(),getAndAdd(),使用方便。更重要的是,Atomic类不是一个简单的同步封装,其内部实现不是简单的使用synchronized,而是一个更为高效的方式CAS (compare and swap) + volatile,从而避免了synchronized的高开销,执行效率大为提高。限于篇幅,关于“CAS”原理就不在这里讲诉。
所以,出于性能考虑,强烈建议尽可能使用Atomic类,而不要去写基于synchronized关键字的代码实现。
最后总结一下,在这个帖子中咱们讲述了一下几个问题:
1. ++操做不是原子操做
2. 非原子操做有线程安全问题
3. 并发下的内存可见性
4. Atomic类经过CAS + volatile能够比synchronized作的更高效,推荐使用