这是一个真实案例,曾经惹出硕大风波,故事的起因却很简单,就是需要实现一个简单的计数器,每次取值然后加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做的更高效,推荐使用

推荐使用concurrent包中的Atomic类的更多相关文章

  1. Java并发(6):concurrent包中的Copy-On-Write容器

    一. concurrent包介绍 在JDK1.5之前,Java中要进行业务并发时,通常需要有程序员独立完成代码实现,而当针对高质量Java多线程并发程序设计时,为防止死蹦等现象的出现,比如使用java ...

  2. 黑马程序员——【Java基础】——File类、Properties集合、IO包中的其他类

    ---------- android培训.java培训.期待与您交流! ---------- 一.File类 (一)概述 1.File类:文件和目录路径名的抽象表现形式 2.作用: (1)用来将文件或 ...

  3. java中常用的包、类、以及包中常用的类、方法、属性----sql和text\swing

    java中常用的包.类.以及包中常用的类.方法.属性 常用的包 java.io.*; java.util.*; java.lang.*; java.sql.*; java.text.*; java.a ...

  4. Java 访问限制符 在同一包中或在不同包中:使用类创建对象的权限 & 对象访问成员变量与方法的权限 & 继承的权限 & 深入理解protected权限

    一.实例成员与类成员 1. 当类的字节码被加载到内存, 类中类变量.类方法即被分配了相应内存空间.入口地址(所有对象共享). 2. 当该类创建对象后,类中实例变量被分配内存(不同对象的实例变量互不相同 ...

  5. javax.Servlet的包中,属于类的是。(选择1项)

    javax.Servlet的包中,属于类的是.(选择1项) A.Servlet B.GenericServlet C.ServletRequest D.ServletContext 解答:B Serv ...

  6. java中的Atomic类

    文章目录 问题背景 Lock 使用Atomic java中的Atomic类 问题背景 在多线程环境中,我们最常遇到的问题就是变量的值进行同步.因为变量需要在多线程中进行共享,所以我们必须需要采用一定的 ...

  7. int是java.lang包中可用的类的名称

    int是java.lang包中可用的类的名称(x) int为基本数据类型,不是类

  8. 24.Java中atomic包中的原子操作类总结

    1. 原子操作类介绍 在并发编程中很容易出现并发安全的问题,有一个很简单的例子就是多线程更新变量i=1,比如多个线程执行i++操作,就有可能获取不到正确的值,而这个问题,最常用的方法是通过Synchr ...

  9. 手动编译含package的java源程序(包含外部包中定义的类)

    1)定义一个GSM类,如下: 包名是“SRC.GSM”,并且此程序引用了外部jar包.使用javac命令对GSM.java进行编译: GSM.java所在的文件夹如下所示: 切换到这个目录为当前工作目 ...

随机推荐

  1. UVA-10074 最大子矩阵 DP

    求出大矩阵里面全为0的最大子矩阵 我自己用的个挫DP写的,感觉写的不是很好,其实可以再优化,DP想法就是以 0 0 到当前 i j 为整体矩阵考虑,当前 i j就是从 i-1 j或者 i,j-1那里最 ...

  2. 熟练使用WebApi开发

    在建立WebApi框架的时候,要想自己的业务需求是什么.例如PC端(前端),APP端都要使用的同一接口,就得考虑Webapi来提供接口支持了.最近公司刚好让我整合一下公司的接口项目(有WebServi ...

  3. 解决在Anaconda中的cv2在pycharm中不可使用的问题

    在Anaconda中已经安装好的opencv模块在pycharm中却不能正常使用,后来发现是pycharm使用的python环境中没有opencv的包,解决方法有两种: 方法一 在pycharm的设置 ...

  4. c#为什么要用事物

    一.事务的定义 所谓事务,它是一个操作集合,这些操作要么都执行,要么都不执行,它是一个不可分割的工作单位.典型的例子就像从网上银行系统的帐户A转帐到帐户B,它经过两个阶段:1.从帐户A取出款项.2.把 ...

  5. <强化学习>开门帖

    (本系列只用作本人笔记,如果看官是以新手开始学习RL,不建议看我写的笔记昂) 今天是2020年2月7日,开始二刷david silver ulc课程.https://www.youtube.com/w ...

  6. Patroni 修改配置

    Patroni 修改配置 背景 使用 Patroni 部署 postgresql 集群的时候,不能单独修改单点的配置,这里需要通过 Patroni 来修改配置. 修改步骤 1. 修改 postgres ...

  7. Android如何制作自己的依赖库上传至github供别人下载使用

    Android如何制作自己的依赖库上传至github供别人下载使用 https://blog.csdn.net/xuchao_blog/article/details/62893851

  8. 利用CSS制作背景变色的横向导航栏

    1.表单 页面如下: <html> <head> <title>注册表单页面</title> </head> <body> &l ...

  9. Complier

    Complier [2019福建省赛] 模拟题应该有信心写,多出一些样例 当/* 与// 在一起的时候总会出错,一旦出现了这些有效的 应该把它删掉不对后面产生影响 #include<bits/s ...

  10. 项目中关于RPC 和rocketMQ使用场景的感受

    在花生待的这半年,切身体会了系统之间交互场景的接口技术实现方式,个人总结.仅供参考: 1.关于rpc接口,一般情况下 都是同步的.A系统的流程调用B系统.等着B返回,根据返回结果继续进行A接下来的流程 ...