Reputation: 570
Java 17 has added a new RandomGenerator
interface. However it seems that all of the new implementations are not thread-safe. The recommended way to use the new interface in multi-threaded situations is to use SplittableRandom
and call split
from the original thread when a new thread is spawned. However, in some situations you don't have control over the parts of the code where the new threads are spawned, and you just need to share an instance between several threads.
I could use Random
but this leads to contention because of all the synchronization. It would also be possible to use ThreadLocalRandom
, but I'm reluctant to do this because this class is now considered "legacy", and because this doesn't give me a thread-safe implementation of RandomGenerator
without a whole load of boilerplate:
new RandomGenerator() {
@Override
public int nextInt() {
return ThreadLocalRandom.current().nextInt();
}
@Override
public long nextLong() {
return ThreadLocalRandom.current().nextLong();
}
...
}
To me this appears to be a fairly fundamental gap in the new API, but I could be missing something. What is the idiomatic Java 17 way to get a thread-safe implementation of RandomGenerator
?
Upvotes: 2
Views: 1368
Reputation: 570
The most direct answer to the question is that the new Java 17 APIs do not provide any direct support for obtaining efficient, thread-safe implementations of RandomGenerator
.
Using the idea from this answer, the following code shows how you could use the new SplittableGenerator
interface to do it, but it's a lot of boilerplate.
import java.util.random.RandomGenerator;
public final class ThreadSafeGenerator implements RandomGenerator {
private final ThreadLocal<RandomGenerator> threadLocal;
public ThreadSafeGenerator(String name) {
SplittableGenerator baseGenerator = SplittableGenerator.of(name);
threadLocal = ThreadLocal.withInitial(() -> {
synchronized (baseGenerator) {
return baseGenerator.split();
}
});
}
@Override
public boolean nextBoolean() {
return threadLocal.get().nextBoolean();
}
@Override
public int nextInt() {
return threadLocal.get().nextInt();
}
// Not finished. All methods of RandomGenerator should be implemented following this pattern.
}
Upvotes: 0
Reputation: 298153
When you don’t have control over the work splitting or creation of threads, the simplest solution from the using site’s perspective, is a ThreadLocal<RandomGenerator>
.
public static void main(String[] args) {
// spin up threads
ForkJoinPool.commonPool().invokeAll(
Collections.nCopies(8, () -> { Thread.sleep(300); return null; }));
doWork(ThreadLocal.withInitial(synching(SplittableGenerator.of("L32X64MixRandom"))));
doWork(ThreadLocal.withInitial(synching(new SplittableRandom())));
doWork(ThreadLocal.withInitial(ThreadLocalRandom::current));
}
static final Supplier<SplittableGenerator> synching(SplittableGenerator r) {
return () -> {
synchronized(r) {
return r.split();
}
};
}
private static void doWork(ThreadLocal<RandomGenerator> theGenerator) {
System.out.println(theGenerator.get().toString());
Set<Thread> threads = ConcurrentHashMap.newKeySet();
var ints = Stream.generate(() -> theGenerator.get().nextInt(10, 90))
.parallel()
.limit(100)
.peek(x -> threads.add(Thread.currentThread()))
.toArray();
System.out.println(Arrays.toString(ints));
System.out.println(threads.stream().map(Thread::getName).toList());
System.out.println();
}
Since this will not split the RNG before handing one over to another thread but from the already existing worker thread, it has to synchronize the operation. But this happens exactly once per thread when the thread local variable is queried the first time. It’s also worth noting the the base RNG is only accessed from that synchronized block.
Note that this also allows the integration of the legacy ThreadLocalRandom.current()
without additional synchronization. It would even work with a synchronizing RNG like Random r = new Random(); doTheWork(ThreadLocal.withInitial(() -> r));
.
Of course, that’s only for illustration, as the RNGs in question have dedicated methods for creating streams which can split before workload is handed over to another worker thread.
Upvotes: 7