Code review comment: Why not simply use an `AsyncCache`? Then you can write this as,  ```java boolean[] isCurrentThreadComputation = { false }; var future = cache.get(key, (k, executor) -> {   isCurrentThreadComputation[0] = true;   return CompletableFuture.supplyAsync(() -> toCacheValue(valueLoader.call()), executor); }); try {   Object value = isCurrentThreadComputation[0]        ? future.get()        : future.get(lockTimeout, TimeUnit.MILLISECONDS);   return fromCacheValue(value); } catch (TimeoutException e) {   return valueLoader.call(); } ```

Code under review:
public Object get(Object key, Callable<Object> valueLoader, long lockTimeout) throws Exception {
        if (lockTimeout <= 0) {
            return fromCacheValue(cache.get(key, new MappingFunction(valueLoader)));
        }

        

        /*
         * If the current key is not already associated with a value in the Caffeine cache, there's no way to know if the
         * current thread or another one is responsible for computing the missing value. While we want to wait at most for the
         * lock timeout delay if the value is being computed by another thread, we also want to ignore the timeout if the value
         * is being computed from the current thread. The following variable will be used to make sure we don't interrupt a
         * current-thread computation.
         */
        AtomicBoolean isCurrentThreadComputation = new AtomicBoolean();

        CompletableFuture<Object> future = CompletableFuture.supplyAsync(() -> {
            return fromCacheValue(cache.get(key, new MappingFunction(valueLoader, isCurrentThreadComputation)));
        });
        try {
            
            return future.get(lockTimeout, TimeUnit.MILLISECONDS);
        } catch (TimeoutException e) {
            
            
            if (!isCurrentThreadComputation.get()) {
                
                
                
                return valueLoader.call();
            }
        }
        
        
        return future.get();
    }

Line commented on: return fromCacheValue(cache.get(key, new MappingFunction(valueLoader, isCurrentThreadComputation)));

Revised code:
public Object get(Object key, Callable<Object> valueLoader, long lockTimeout) throws Exception {
        if (lockTimeout <= 0) {
            return fromCacheValue(cache.synchronous().get(key, k -> new MappingSupplier(valueLoader).get()));
        }

        

        /*
         * If the current key is not already associated with a value in the Caffeine cache, there's no way to know if the
         * current thread or another one started the missing value computation. The following variable will be used to
         * determine whether or not a timeout should be triggered during the computation depending on which thread started it.
         */
        boolean[] isCurrentThreadComputation = { false };

        CompletableFuture<Object> future = cache.get(key, (k, executor) -> {
            isCurrentThreadComputation[0] = true;
            return CompletableFuture.supplyAsync(new MappingSupplier(valueLoader), executor);
        });

        if (isCurrentThreadComputation[0]) {
            
            
            return fromCacheValue(future.get());
        } else {
            
            
            try {
                return fromCacheValue(future.get(lockTimeout, TimeUnit.MILLISECONDS));
            } catch (TimeoutException e) {
                
                
                
                return valueLoader.call();
            }
        }
    }