论坛首页 Java企业应用论坛

编码最佳实践(2)--推荐使用concurrent包中的Atomic类

浏览 4806 次
精华帖 (1) :: 良好帖 (13) :: 新手帖 (1) :: 隐藏帖 (4)
作者 正文
   发表时间:2012-06-16  

    这是一个真实案例,曾经惹出硕大风波,故事的起因却很简单,就是需要实现一个简单的计数器,每次取值然后加1,于是就有了下面这段代码:

	  private int counter = 0;
          public int getCount ( ) {
                   return counter++;
          }


    这个计数器被用于生成一个sessionId,这个sessionID用于和外部计费系统交互,这个sessionId理所当然的要求保证全局唯一而不重复。但是很遗憾,上面的代码最终被发现会产生相同的id,因此会造成一些请求莫名其妙的报错.....更痛苦的是,上面这段代码是一个来自其他部门开发的工具类,我们当时只是拿了它的jar包来调用,没有源码,更没有想这里面会有如此低级而可怕的错误。

    由于重复的sessionId,造成有个别请求失败,虽然出现概率极低,经常跑一天测试都不见得能重现一次。因为是和计费相关,因此哪怕是再低的概率出错,也不得不要求解决。实际情况是,项目开发到最后阶段,都开始做发布前最后的稳定性测试了,在7*24小时的连续测试中,这个问题往往在测试开始几天后才重现,将当时负责trouble shooting的同事折腾的很惨......经过反复的查找,终于有人怀疑到这里,反编译了那个jar包,才看到上面这段出问题的代码。

    这个低级的错误,源于一个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()方法上做同步:

	  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线程安全总结", http://www.iteye.com/topic/806990。

    解决这里的"内存可见性"问题的方式有两个,一个是继续使用 synchronized 关键字,代码如下

	  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做的更高效,推荐使用
   发表时间:2012-06-18  
同步也不行的,要从架构设计上去修改。不然,如果部署在负载均衡、集群中,会出问题的吧?
  • 大小: 150.8 KB
0 请登录后投票
   发表时间:2012-06-18  
anhaoy 写道
同步也不行的,要从架构设计上去修改。不然,如果部署在负载均衡、集群中,会出问题的吧?


那是另外一个话题了,关于如何生成一个id,这个counter只是id组成的一部分,还有其他因素的。

0 请登录后投票
   发表时间:2012-06-19  
严重同意,volatile确实在性能方面比synchronized要高哦
0 请登录后投票
   发表时间:2012-06-19   最后修改:2012-06-19
anhaoy 写道
同步也不行的,要从架构设计上去修改。不然,如果部署在负载均衡、集群中,会出问题的吧?


不需要在任何设计的初期都往集群上想。否则你会发现你大部分代码都要重新设计。
为了一个虚无缥缈的可能性而付出巨大的代价,反倒是一件得不偿失的事情。

项目在初期应有负载的预估。

这个真的是另外一个话题了。


PS:
不知道为什么这个帖子有1票隐藏,1票新手。
也许你们觉得这个问题简单,但毕竟是人家经历了一翻trouble shooting的功夫。而且这里边也提到了Atomic类里边的原理。 那个投反对票的人,你就都那么清楚吗?
0 请登录后投票
   发表时间:2012-06-19   最后修改:2012-06-19
ThinkingQuest 写道

PS:
不知道为什么这个帖子有1票隐藏,1票新手。
也许你们觉得这个问题简单,但毕竟是人家经历了一翻trouble shooting的功夫。而且这里边也提到了Atomic类里边的原理。 那个投反对票的人,你就都那么清楚吗?


我也好奇怪,Atomic类的使用很简单,但是能明白这些Atomic类背后的故事,就不容易了

谁投的新手和隐藏?麻烦站出来给出你的理由好不好?
0 请登录后投票
   发表时间:2012-06-19   最后修改:2012-06-19
skydream 写道
ThinkingQuest 写道

PS:
不知道为什么这个帖子有1票隐藏,1票新手。
也许你们觉得这个问题简单,但毕竟是人家经历了一翻trouble shooting的功夫。而且这里边也提到了Atomic类里边的原理。 那个投反对票的人,你就都那么清楚吗?


我也好奇怪,Atomic类的使用很简单,但是能明白这些Atomic类背后的故事,就不容易了

谁投的新手和隐藏?麻烦站出来给出你的理由好不好?


可能觉得,刚学习多线程时,就讲过  ++ -- 之类的操作不是原子操作吧。他们觉得这个是最基本的东西。
看个开头就投票的人,应该不少吧。
0 请登录后投票
   发表时间:2012-06-19  
anhaoy 写道

可能觉得,刚学习多线程时,就讲过  ++ -- 之类的操作不是原子操作吧。他们觉得这个是最基本的东西。
看个开头就投票的人,应该不少吧。


还真有可能,看来我的改改写文章的风格了
0 请登录后投票
   发表时间:2012-06-19  
我觉得 skydream 能够给大家很好的解释Atomic的使用原因 以及思考过程 ,这些就已经很宝贵了 需要学习
0 请登录后投票
论坛首页 Java企业应用版

跳转论坛:
Global site tag (gtag.js) - Google Analytics