Skip to content

Instantly share code, notes, and snippets.

@Genzer
Created June 13, 2025 11:42
Show Gist options
  • Save Genzer/685c1d17e4070cfe3b9e973767749b12 to your computer and use it in GitHub Desktop.
Save Genzer/685c1d17e4070cfe3b9e973767749b12 to your computer and use it in GitHub Desktop.
An attempt to make a simpler implementation of cyclops.function.Memoize.memoizeSupplier (make it lazy)
import java.util.concurrent.atomic.*;
import java.util.concurrent.*;
import java.util.function.*;
import java.util.stream.*;
import java.util.*;
import cyclops.function.*;
/**
* This class is my attempt to see if I can make an implementation of cyclops's Memoize.memoizeSupplier
* simpler without relying on `synchronize` keyword (as the actual implementation), or `ConcurrentHashMap#computeIfAbsent`
* as the author's article.
*
* Unfortunately, both mine and theirs encounter a problem with multiple threads reading the Supplier.
*/
public class MemoizeSupplier {
public static void main(String... args) throws Exception {
//Supplier<Long> m = memoizeSupplier(() -> System.nanoTime());
var logExecutor = Executors.newFixedThreadPool(2);
// Even the library itself has this problem. Why?
//
var m = Memoize.memoizeSupplier(() -> {
logExecutor.submit(() -> System.out.println("Supplier is called at " + System.currentTimeMillis()));
return System.nanoTime();
});
int tasksSize = 100;
var tasks = new ArrayList<Callable<Long>>(tasksSize);
var doneSignal = new CountDownLatch(tasksSize);
IntStream.rangeClosed(1, tasksSize).forEach(i -> {
tasks.add(() -> {
logExecutor.submit(() -> System.out.println("Tasks " + i + " is started at " + System.currentTimeMillis()));
var result = m.get();
logExecutor.submit(() -> System.out.println("Tasks " + i + " is done at " + System.currentTimeMillis()));
doneSignal.countDown();
logExecutor.submit(() -> System.out.println("Tasks " + i + " counts down at " + System.currentTimeMillis()));
return result;
});
});
var executor = Executors.newFixedThreadPool(4);
var runningTasks = executor.invokeAll(tasks);
System.out.println("Running tasks: " + tasks.size());
doneSignal.await();
var tasksDoneCount = runningTasks.stream()
.filter(f -> f.isDone())
.map(MemoizeSupplier::collect)
.count();
var result = runningTasks.stream()
.filter(f -> f.isDone())
.map(MemoizeSupplier::collect)
.distinct()
//.filter(v -> v == null)
.toList();
System.out.println("Result: " + result + " after " + tasksDoneCount + " tasks.");
executor.shutdown();
logExecutor.shutdown();
}
private static Long collect(Future<Long> future) {
try {
return future.get();
} catch (Exception any) {
throw new RuntimeException(any);
}
}
public static <T> Supplier<T> memoizeSupplier(Supplier<T> supplier) {
/*
* In the Cyclops article [1], a `ConcurrentHashMap` is used because of `computeIfAbsent`'s nature.
* In the real implementation, the Memoize#memoizeSupplier() uses `AtomicReference` but an extra `synchronize`
* is used on the atomic reference itself to prevent other threads from setting it.
*
* THIS implementation seeks to avoid the heaviness of `ConcurrentMap` and the synchronization mechanism.
*
* [1]: https://medium.com/@johnmcclean/dysfunctional-programming-in-java-i-laziness-cc9c6981de39
*/
final AtomicBoolean initialized = new AtomicBoolean(false);
final AtomicReference<T> value = new AtomicReference<>();
return () -> {
if (initialized.compareAndSet(false, true)) {
value.set(supplier.get());
return value.get();
}
return value.get();
};
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment