Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace synchronized with j.u.c.l.ReentrantLock for Loom #3480

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

babanin
Copy link

@babanin babanin commented Jul 13, 2023

Goal

In Sep 2023, JDK 21 will be released. One of the most anticipated feature - Project Loom aka Virtual threads.

One of the problems with the current implementation, are synchronized blocks:

However, not all Java constructs have been retrofitted that way. Such operations include synchronized methods and code blocks. Using them causes the virtual thread to become pinned to the carrier thread. When a thread is pinned, blocking operations will block the underlying carrier thread—precisely as it would happen in pre-Loom times.

From here: https://softwaremill.com/what-is-blocking-in-loom/#synchronization-problems

The main goal of this PR is to replace synchronized blocks with ReentrantLocks who are not susceptible to this problem.

Approach

My approach was quite simple: replace synchronized blocks with ReentrantLock's. However, in a few cases, though, I believe we can get rid of synchronization at all:

  1. This code was written by @clebersonp:
 private JsonObjectMapper getJsonObjectMapper() {
    JsonObjectMapper localRef = this.jsonObjectMapper;
    if (Objects.isNull(localRef)) {
      synchronized (this) {
        localRef = this.jsonObjectMapper;
        if (Objects.isNull(localRef)) {
          this.jsonObjectMapper = localRef = new DefaultGsonObjectMapper();
        }
      }
    }
    return localRef;
  }

I replaced it with:

  private JsonObjectMapper getJsonObjectMapper() {
    if (Objects.isNull(this.jsonObjectMapper)) {
        this.jsonObjectMapper = new DefaultGsonObjectMapper();
    }
    
    return this.jsonObjectMapper;
  }

The field jsonObjectMapper marked as volatile, thus we establish connection (acquire-release) between thread which writes to this field and other threads which will read this field.

However, there is still a scenario when multiple threads will enter this method at the same time and see null: all of them will create an object and write it to a variable. This won't cause any problems since the DefaultGsonObjectMapper is stateless, but the object creation operation itself may be more costly than lock acquisition. Do you think it is worthwhile to return the lock here?

  1. This code was written by @sazzad16:
  private void createBuilder(String graphName) {
    synchronized (builders) {
      builders.putIfAbsent(graphName, new ResultSetBuilder(new GraphCacheImpl(graphName)));
    }
  }

Since builders is ConcurrentHashMap, we can avoid synchronization and either use putIfAbsent or, if creation of ResultSetBuilder and GraphCacheImpl is expensive, use computeIfAbsent:

builders.computeIfAbsent(graphName, graphNameKey -> new ResultSetBuilder(new GraphCacheImpl(graphNameKey)));

Alternative

It worth mentioning a comment on Reddit left by Ron Pressler (Project Loom lead) that the work on making synchronized not pin is underway.

Leave synchronized blocks as is and wait for next version of JDK (22+).

@stevenzyang
Copy link

@babanin do you know if the work done in these synchronized blocks is large enough to cause significant impact?

@pmverma
Copy link

pmverma commented Dec 27, 2023

I would love to see this get merged!

@sazzad16 sazzad16 added this to the 5.2.0 milestone Jan 1, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants