### API(s)

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


### How do you want it to be improved?

[This load](https://github.com/google/guava/blob/4f12c5891a7adedbaa1d99fc9f77d8cc4e9da206/guava/src/com/google/common/cache/LocalCache.java#L2191C1-L2191C71) 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

```java
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

- [X] I agree to follow the [code of conduct](https://github.com/google/.github/blob/master/CODE_OF_CONDUCT.md).

- [X] I have read and understood the [contribution guidelines](https://github.com/google/guava/wiki/HowToContribute#feature-requests).

- [X] I have read and understood [Guava's philosophy](https://github.com/google/guava/wiki/PhilosophyExplained), and I strongly believe that this proposal aligns with it.

ReferenceEntry<K, V> first = table.get(index);
        for (e = first; e != null; e = e.getNext()) {
          K entryKey = e.getKey();
          if (e.getHash() == hash
              && entryKey != null
              && map.keyEquivalence.equivalent(key, entryKey)) {
            valueReference = e.getValueReference();
            if (valueReference.isLoading()) {
              createNewEntry = false;
            } else {
              V value = valueReference.get();
              if (value == null) {
                enqueueNotification(
                    entryKey, hash, value, valueReference.getWeight(), RemovalCause.COLLECTED);
              } else if (map.isExpired(e, now)) {
                
                
                enqueueNotification(
                    entryKey, hash, value, valueReference.getWeight(), RemovalCause.EXPIRED);
              } else {
                recordLockedRead(e, now);
                statsCounter.recordHits(1);
                
                return value;
              }
              
              writeQueue.remove(e);
              accessQueue.remove(e);
              this.count = newCount; 
            }