Marc's Blog

About Me

My name is Marc Brooker. I've been writing code, reading code, and living vicariously through computers for as long as I can remember. I like to build things that work. I also dabble in machining, welding, cooking and skiing.

I'm currently an engineer at Amazon Web Services (AWS) in Seattle, where I work on databases, serverless, and serverless databases. Before that, I worked on EC2 and EBS.
All opinions are my own.

Links

My Publications and Videos
@marcbrooker on Mastodon @MarcJBrooker on Twitter

Highly contended and fair locking in Java

How do explicit locks compare to volatile access?

In my last post on Java’s volatile, I showed how (in one set of experiments) Java volatile variable reads don’t come for free. The cost of accessing a highly-contended volatile variable in one micro-benchmark came out at about 100x the cost of accessing a non-volatile variable. How does that compare to locking?

In the last post, I presented the results of a program which:

For this post, I modified it to make the variable non-volatile, and add explicit locking using a ReentrantLock. Performance dropped substantially bad. The graph below is an update of the graph from the last post, including the locking version.

Graph of lock results

For reads, the locking version of this test is about 33x more expensive than the volatile version (and over 3000x more than the incorrect unsynchronized version). Writes are about 15x more expensive. To put this in perspective, it’s still only 545 nanoseconds per lock operation, so individual operations are not really expensive in absolute terms.

The other effect of locking vs. volatile is starvation of threads. The documentation for ReentrantLock says:

The constructor for this class accepts an optional fairness parameter. When set true, under contention, locks favor granting access to the longest-waiting thread. Otherwise this lock does not guarantee any particular access order. Programs using fair locks accessed by many threads may display lower overall throughput (i.e., are slower; often much slower) than those using the default setting, but have smaller variances in times to obtain locks and guarantee lack of starvation.

In my tests, I had fairness disabled and saw significant thread starvation. In about one run in five, the program actually runs in a totally serial order - each thread running to completion before any of the other threads run. I have seen thread starvation issues in real-world code too, but never to this extent. Fair locking fixed this problem, but increased the per-lock time to 32µs (from 0.5µs for the non-fair version). That’s an increase of about 60x, for a total of approximately 200x more expensive than a volatile access.

Fair locks in the version of Java I was using are based on the AbstractQueuedSynchronizer. It uses a type of lock queue modified from Craig, Landin, Hagersten locks, which provide a FIFO queue of waiters on a lock without needing to depend on another lower-level locking primitive. It’s truly fascinating stuff, and well worth reading.

Given the very high costs of fair locks under contention, it’s probably best to avoid them in those situations. There are situations where they work well and are very necessary. From Doug Lea:

Even though they may perform poorly under high contention when protecting briefly-held code bodies, fair locks work well, for example, when they protect relatively long code bodies and/or with relatively long inter-lock intervals

It’s probably best to avoid writing high-contention code unless absolutely necessary. Where it is necessary, however, volatile should be tested against locking, because it’s likely to be much faster. Fair locking shouldn’t be used in performance sensitive code unless its guarantees are really needed. You’ll likely want to do your own testing, and not assume your application will behave like my test.