我喜欢新鲜玩意儿,而Java 8里面就有很多。这回我准备介绍一下个人一个最爱——并发计数器。这是一组新的类,用于维护多个线程并发读写的计数器。新的API带来了显著的性能提高,同时还保证了接口的简单易用。git
多核时代来临了以后,你们都开始使用并发计数器,咱们先来看一下Java迄今为止提供了哪些实现方式,它们的性能和这个新的API相比,又有什么不一样。github
脏计数器——选择这种方式意味着多线程直接并发读写一个普通对象或者静态字段。不幸的是,这么作是行不通的。有两个缘由,一个是在Java里, A+= B操做不是原子的。若是你打开编译后的字节码看一下,你会发现至少有四条指令——第一条是从堆里将字段值加载到线程栈里,第二条是加载要增长的值,第三条指令将它们进行相加,第四条则将结果写回到字段中。数据库
若是多个线程同时在同一个内存位置进行这个操做,你的一个写操做颇有可能就丢掉了,由于另外一个线程可能会覆盖了它的值。还有一个很恶心的事就是这个值的可见性。下面还会详细介绍到。小程序
新手很是容易犯这样的错误,而这样的问题却很难发现。若是你发现团队中有人这么作,最好能帮我个小忙。在你的数据库里面搜一下个人名字“Tal Weiss"。若是你发现我在里面——赶忙把个人记录删掉。这样我会感受舒服点。多线程
synchronzied——这是最基础的同步操做了,只要你在读写值,它就会阻塞住其它的全部线程。这种方式的确行得通,不过确定的是,你的程序运行起来会像DMV排的长队那样。并发
读写锁(RWLock)——这个和基础的Java锁相比就巧妙了些,它可让你区分出那些要修改值所以须要阻塞别人的进程以及那些只是读取值不须要进入临界区的。虽然这个方法有的时候很高效(好比写线程的数量比较少的话),但仍是至关无语,由于当你获取写锁的时候仍是会阻塞住其它线程的执行。高并发
volatile——这个常常会被误用的关键字会让JIT编译中止在运行时进行机器码的优化工做,所以字段一旦有更新别的线程立刻就能看到。性能
它会使得JIT编译器常常玩的一些把戏好比说调整赋值语句的顺序这些没法进行。JIT编译器有可能会改变字段的赋值顺序。什么,你再说一遍?是的,你听的没错。这个神秘的小把戏使得它能够减少程序访问全局堆的次数,同时它还能保证不会影响到你的程序的执行。这真是有点偷偷摸摸的感受。测试
那何时应该使用volatile计数器?若是你只有一个线程在更新一个值,而多个线程在读的话,这是个很合适的场景。由于彻底没有竞争。优化
你可能会问为何都使用它就完了?由于若是有多个线程在更新的话就会有问题了。因为A+=B不是一个原子操做,这么作的话可能会覆盖掉别人写的话。在Java 8之前,这种状况你就只能用AtomicInteger了。
AtomicInteger——这组类使用了处理器的CAS (compare-and-swap)指令来更新计数器的值。听起来不错吧?一半一半吧。因为它直接使用机器指令来设置值,所以对其它线程的影响最小。很差的一面是若是它和别的线程有竞争赋值失败了,它会继续重试。在高并发的条件 下,这就成了一个自旋锁,线程会在一个无限的循环内不断的尝试赋值,直到成功为止。咱们可不太想看到这种局面。Java 8来了,还带来了LongAdders。
Java 8 Adders——这是个很是棒的新的API,我对它的仰慕有如滔滔江水连绵不绝。从使用者的角度来讲,它很像AtomicInteger。只须要建立一个LongAdder对象,而后使用intValue()以及add()方法来获取和设置它的值。而奇迹就发生在这一切的背后。
若是因为竞争这个类的CAS操做失败了的话,它会要添加的值存到一个线程本地的内部的cell对象里。当intValue()方法调用 的时候,它把这些cell的值加到总和里。这样就减小了CAS重试或者阻塞别的线程的状况。真不错的想法。
说的也差很少了。咱们来看看它的真本事。咱们作了以下的一个基准测试:把一个计数器设置为0,而后多个线程开始读取并进行自增。当计数器到达10^8的时候中止。咱们在一个4核的i7处理器上运行这个测试。
我用了10个线程来运行这个基准测试——读写分别使用5个线程来进行,这样的话会出现严重的竞争条件:
注意:脏读和volatile都有可能产生脏值。
测试的代码在这里。
结论
若是你已经在代码里使用到它了——我会感到很是高兴。