本文转载自微信公众号《JerryCodes》,作者KyleJerry。转载本文请联系JerryCodes公众号。AtomicLong存在的问题LongAdder带来的改进和原理如何选择AtomicLong能否被LongAdder替代?结语最近,秋招陆续开始。有读者给我反馈了一道真实的面试题,关于高并发下AtomicInteger的性能。在上一篇文章中,我们提到了原子类Atomic族的原子族,族要整齐。我们知道在JDK1.5中增加了对应于并发情况下使用的Integer/Long的原子类AtomicInteger和AtomicLong。在并发场景下,如果我们需要实现计数器,可以使用AtomicInteger和AtomicLong。这样,我们就可以避免加锁和复杂的代码逻辑。有了它们,我们只需要执行相应封装的方法,例如对这两个变量进行原子自增或原子自减操作即可满足大部分业务场景的需求。然而,虽然它们非常有用,但是如果你的业务场景有很大的并发量,那么你也会发现这两个原子类实际上会有很大的性能问题。让我们从一个例子开始。AtomicLong的问题首先我们来看一段代码:/***描述:16线程下使用AtomicLong*/publicclassAtomicLongDemo{publicstaticvoidmain(String[]args)throwsInterruptedException{AtomicLongcounter=newAtomicLong(0);ExecutorServiceservice=Executors.newFixedThreadPool(16);for(inti=0;i<100;i++){service.submit(newTask(counter));}Thread.sleep(2000);System.out.println(counter.get());}staticclassTaskimplementsRunnable{privatefinalAtomicLongcounter;publicTask(AtomicLongcounter){this.counter=counter;}@Overridepublicvoidrun(){counter.incrementAndGet();}}}从这段代码可以看出,我们创建了一个新的AtomicLong,其原始值为0。然后,有是一个有16个线程的线程池,同一个任务被添加到这个线程池中100次。那我们再往下看这个任务是什么。在下面的Task类中可以看到,这个任务其实就是每次调用AtomicLong的incrementAndGet方法,相当于自加操作。这样,整个类的作用就是从0开始这个原子类,添加100个任务,每个任务添加一次。这段代码的运行结果无疑是100,虽然是多线程并发访问,但是AtomicLong还是可以保证incrementAndGet操作的原子性,所以不会有线程安全问题。但如果我们更进一步,看看里面,你可能会感到惊讶。我们将模型简化为只有两个线程同时工作的并发场景,因为两个线程和多个线程本质上是一样的。如图:在这张图中我们可以看到,每个线程都运行在自己的核心中,它们都有自己独占的本地内存。在本地内存下方,有两个CPU内核共享的共享内存。对于AtomicLong内部的value属性,即保存当前AtomicLong的值的属性,是通过volatile修饰的,所以需要保证自身的可见性。这样,每次它的值发生变化,都需要进行flush和刷新。比如一开始,ctr的值为0,那么如图所示,一旦core1将其变为1,它会先将这个1的最新结果flush到左边下方的共享内存中。然后,去右边刷新core2的本地内存。这样对于core2来说,它会感知到这个变化。由于竞争激烈,这种flush和refresh操作会消耗大量资源,CAS经常会失败。LongAdder带来的改进和原理在JDK8中增加了LongAdder类,这是一个针对Long类型的操作工具类。那么既然我们已经有了AtomicLong,为什么还要添加LongAdder这样的类呢?我们也用一个例子来说明。下面的示例与前面的示例非常相似,只是我们将工具类从AtomicLong更改为LongAdder。另一个区别是,当最终打印出结果时,调用的方法已经从原来的get方法变成了现在的sum方法。其余的逻辑是一样的。下面看一个使用LongAdder的代码示例:/***说明:16线程下使用LongAdder*/publicclassLongAdderDemo{publicstaticvoidmain(String[]args)throwsInterruptedException{LongAddercounter=newLongAdder();ExecutorServiceservice=Executors.newFixedThreadPool(16);for(inti=0;i<100;i++){service.submit(newTask(counter));}Thread.sleep(2000);System.out.println(counter.sum());}staticclassTaskimplementsRunnable{privatefinalLongAddercounter;publicTask(LongAddercounter){this.counter=counter;}@Overridepublicvoidrun(){counter.increment();}}}代码运行结果也是100,但是运行速度比刚才AtomicLong的实现要快。下面解释一下为什么在高并发下LongAdder比AtomicLong效率更高。因为LongAdder引入了分段累加的概念,所以涉及到计数的内部参数有两个:第一个叫base,是一个变量,第二个是Cell[],是一个数组。base在比赛不激烈的时候使用,累加结果可以直接改成base变量。那么,竞争激烈的时候,就会用到我们的Cell[]数组。一旦竞争激烈,各个线程就会打散添加到自己对应的Cell[]数组的某个对象中,而不是共享同一个。这样LongAdder会把不同的线程映射到不同的Cell进行修改,减少冲突的概率。这是一个提高并发性的分段概念,类似于Java7中的ConcurrentHashMap的16Segments,思路类似。当竞争激烈时,LongAdder会通过计算每个线程的hash值,将线程分配给不同的Cell。每个Cell相当于一个独立的计数器,这样不会干扰其他计数器。它们之间没有竞争关系,所以在自增的过程中,大大减少了刚才的flush和refresh,减少了冲突的概率,因为它有多个计数器同时工作,所以占用的内存也相对较小较大。那么LongAdder到底是如何实现多线程计数的呢?答案就在最后一步的sum方法中。当执行LongAdder.sum()时,会将每个线程中的Cells相加,并与base相加,形成最终的Sum。代码如下:publiclongsum(){Cell[]as=cells;Cella;longsum=base;if(as!=null){for(inti=0;i
