Skip to content

remove synchronized from LocalCache::get(key, loader) to allow for VirtualThread-friendly value-loading #6845

@cortlepp

Description

@cortlepp

API(s)

com.google.common.cache.LocalCache$Segment.lockedGetOrLoad

How do you want it to be improved?

This load should be possible without using synchronized.

Why do we need it to be improved?

Using synchronized here is not performant when the current Thread is a Java 21 VirtualThread and the loader does some blocking operation, because the synchronized prevents the JVM from unmounting the Thread. The performance could be drastically improved by using p.e. ReentrantLock.

Example

Thread.ofVirtual().start( () -> {
  Database database;
  var databaseLoader = new CacheLoader<String, Object>() {
        @Override
        public Object load(String key) {
            return database.load(key); // loads an object by querying a database over the network 
        }
    };
var cache = CacheBuilder.newBuilder().build(databaseLoader);
var fooObject = cache.getOrLoad("foo"); // cache miss, invokes the loader
});

Current Behavior

In the above example, the loader is invoked. This invocation happens inside a synchronized block in com.google.common.cache.LocalCache$Segment.lockedGetOrLoad. Because the loader does I/O but is inside the synchronized block the VirtualThread can not be unmounted and therefore blocks its carrier. This is bad for the throughput of the application. This behavior can be observed using any loader that does a blocking operation when it is invoked from a VirtualThread (just start your JVM with -Djdk.tracePinnedThreads=full). The performance impact is dependent on how long the loader takes, but since generally one only caches things that are expensive to do I believe this to be a problem for pretty much anyone using LocalCache in combination with VirtualThreads.

Desired Behavior

com.google.common.cache.LocalCache$Segment.lockedGetOrLoad should work gracefully with VirtualThreads and not block the carrier Thread if a loader does I/O or some other blocking operation.

The method should therefore be refactored to lock using some other method, p.e. a ReentrantLock.

Concrete Use Cases

We use a LocalCache to cache frequently used database objects. Our loader therefore executes a SQL query over the network, which is a blocking operation and takes comparatively long. We are currently experimenting with using VirtualThreads in order to improve the throughput of I/O heavy workloads.

Checklist

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions