最近遇到高并发引起的性能问题,最终定位到的问题是 LinkedBlockingQueue
的性能不行,最终通过创建了多个 Queue 来减少每个 Queue 的竞争压力。人生中第一次遇到 JDK 自带数据结构无法满足需求的情形,决心好好研究一下为什么。
压测在一个 40 个核的机器上,tomcat 默认 200 个线程,发送方以 500 并发约 1w QPS 发送请求,要求999 分位的响应在 50ms 左右。代码中有一个异步写入数据库的任务,实际测试时有超过 60% 的延时都在写入队列中(实际上是往 ThreadPool 提交任务)。于是开始调研 LinkedBlockingQueue
的实现。
LinkedBlockingQueue 相当于是普通 LinkedList 加上 ReentrantLock
在操作时加锁。而 ReentrantLock (以及其它 Java 中的锁)内部都是靠 CAS 来实现原子性。而 CAS 在高并发时因为线程会不停重试,所以理论上性能会比原生的锁更差。
实际上想对比 CAS 和原生锁是很困难的。Java 中没有原生的锁,而 synchronized
有 JDK 的各种优化,在一些低并发的情况下也用到了 CAS。对比过 synchronized
和 Unsafe.compareAndSwapInt
发现 CAS 被吊打。所以最后还是退而求其次对比 ReentrantLock
和 Synchronized
的性能。
一个线程竞争 ReentrantLock
失败时,会被放到等待对列中,不会参与后续的竞争,因此 ReentrantLock 不能代表 CAS 在高并发下的表现。不过一般我们也不会直接使用 CAS,所以测试结果也凑合着看了。
测试使用的是 JMH 框架,号称能测到毫秒级。运行的机器是 40 核的,因此至少能保证同时竞争的线程是 40 个(如果 CPU 核数不足,尽管线程数多,真正同时并发的量可能并不多)。JDK 1.8 下测试。
首先测试用 synchronized
与 ReentrantLock
同步自增操作,测试代码如下:
@Benchmark @Group("lock") @GroupThreads(4) public void lockedOp() { try { lock.lock(); lockCounter ++; } finally { lock.unlock(); } } @Benchmark @Group("synchronized") @GroupThreads(4) public void synchronizedOp() { synchronized (this) { rawCounter ++; } }
结果如下:
自增操作 CPU 时间太短,适当增加每个操作的时间,改为往 linkedList 插入一个数据。代码如下:
@Benchmark @Group("lock") @GroupThreads(2) public void lockedOp() { try { lock.lock(); lockQueue.add("event"); if (lockQueue.size() >= CLEAR_COUNT) { lockQueue.clear(); } } finally { lock.unlock(); } } @Benchmark @Group("synchronized") @GroupThreads(2) public void synchronizedOp() { synchronized (this) { rawQueue.add("event"); if (rawQueue.size() >= CLEAR_COUNT) { rawQueue.clear(); } } }
结果如下:
这个测试让我对 ReentrantLock 有了更多的信心,不过一般开发时还是建议用 synchronized, 毕竟大佬们还在不断优化中(看到有文章说 JDK 9 中的 Lock 和 synchronized 已经基本持平了)。
如果有人知道怎么更好地对比 CAS 和锁的性能,欢迎留言~