Created
June 13, 2025 11:42
-
-
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)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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