cpp beginner
cpp beginner

Reputation: 570

Thread-Safe RandomGenerator in Java 17

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

Answers (2)

cpp beginner
cpp beginner

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

Holger
Holger

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

Related Questions