Java concurrency utilities have kept evolving and provides many different ways to achieve similar tasks. Recently, we had a task to implement a concurrent counter. This triggered my interest in comparing different ways and their performance under various read and write workload.
The end result is a simple concurrent counter implemented in various ways:
- ReentrantLock in regular and fair mode
- ReentrantReadWriteLock in regular and fair mode
- StampedLock in regular read/write and optimistic read lock
- Semaphore in regular and fair mode
The benchmark is implemented using JMH, the standard way for reliable Java performance microbenchmark. You can find several really nice tutorials on JMH in the References section.
In my benchmark, there are write and read operations on the counter. The write takes 10ms and read takes 2ms. I set the number of read and write threads to simulate different mix of the workload scenarios using JMH group.
Both the source code and benchmark raw data, Excel sheets and visualizations can be found in the git repo: java-concurrency-counters-benchmark.
Here is a quick summary based on my experiment (I only set 2 rounds of warmups and 2 rounds of benchmark due to limited time):
- AtomicLong and LongAdder has similar throughput. In read-heavy workloads, AtomicLong has better read and write throughput than LongAdder. In write-heavy workloads, LongAdder has slightly better write throughput.
- Fair lock has lower throughput than regular lock in general, but not always.
- Consider using ReentrantLock or ReentrantReadWriteLock if you need high read throughput and the concurrency level is high.
- StampedLock provides very good write throughput in all the read-write mixes, if write throughput is important to you, you can try it. At the same time, if you need comparatively good read throughput, try optimistic read StampedLock. It has really good read throughput when concurrency level is high compared with regular StampedLock.