Java中各种同步机制的性能测试
Java提供了多种同步机制,比如AtomicLong、LongAdder、ReentrantLock等,但是他们的性能到底如何?网上也有一些性能测试文章,但是要么同步机制不全,要么场景不全,因此笔者设计了一个性能测试框架,支持多场景、多种同步机制的性能测试,代码请参见locksbenchmark,后续有时间了再通过JMH进行对比测试。
测试原理
既然是同步测试,当然要使用多线程才行,而且最好覆盖多种场景,总结起来本次测试有以下几个关键点:
- 可以指定读、写线程数,来模拟读多写少、读少写多,读写等比等场景。
- 通过
CyclicBarrier
保证,所有线程开始工作那一刻才启动计时。通过CountDownLatch
保证,只要有一个线程达成目标,就结束计时,保证计时尽量准确。 - 每种同步机制,经过多轮测试,结果去掉最小值、最大值然后取平均值,避免JVM未预热出现奇异值。
- 同步代码块中的代码非常简单,就是累加long型数值到一个目标值。如果需要测试新的同步机制,只需要扩展
Counter
类即可,具体实现可以参照类Raw。
使用方式
代码托管在github,使用maven工具构建,具体命令如下:
1 | git clone git@github.com:chenpy228/locksbenchmark.git |
测试结果
测试环境是Ubuntu 14.04.5 LTS,内核版本是4.4.0-101-generic,CPU是2核,JVM信息如下:
1 | java -version |
最终的测试结果如下,为了方便对比,Raw为没有使用任何同步机制的测试结果:
由于部分同步机制的运行时间相差太大,所以对坐标轴进行了对数化(log10)处理,绘制图表如下(越短越好):
去掉公平锁和读写锁后绘图如下(越短越好):
结论
- 公平锁
ReentrantLock(true)
,即上图中的FairReentrantLock,在所有场景下的表现都比较差,所以在实际应用中很少看到它的身影。如果必须要用,一定要经过谨慎测试。 - 读写锁
ReentrantReadWriteLock
,即上图中的RWLock,在读多写少的场景下表现的比较差,这跟我们的直觉可能不符,所以在java8中又引入了升级版的读写锁StampedLock。当然在写多读少的情况下,表现比较好,基本跟StampedLock
持平。 - 在所有场景下,
AtomicLong
、LongAdder
和非公平锁ReentrantLock(false)
的表现相差不大,而且AtomicLong
略有优势。按照文档的说法,只有竞争非常激烈的时候,LongAdder
才更有优势,读写比20:20估计还不是竞争激烈的时候,针对这两个类补充测试了读写比为200:200场景,结果显示LongAdder
更胜一筹。目前来看,用AtomicLong
可以满足需求,而且个人认为AtomicLong
的接口更丰富。 - 再来看看
StampedLock
,在读多写少的场景下,它的乐观锁(即上图中的OptimisticStampedLock)确实更胜一筹,其他场景下悲观锁略好一些。 - 最后看
synchronized
,整体跟StampedLock
表现差不多。 - 基本上,
AtomicLong
和LongAdder
属于第一梯队,synchronized
、StampedLock
和非公平锁ReentrantLock(false)
属于第二梯队,ReentrantReadWriteLock
和公平锁ReentrantLock(true)
属于第三梯队。
建议
如果被保护的是基本数据类型,在大多数场景下,建议使用Atomic*
系列锁,如果竞争非常激烈可以使用LongAdder
。如果被保护的是复杂的数据结构,在读多写少的场景下,可以用StampedLock
的乐观锁,在读少写多的场景下,可以用StampedLock
的悲观锁,其他场景直接用ReentrantLock
的非公平锁即可。