Search code examples
javamultithreadingperformancerandomthread-local

ThreadLocalRandom with shared static Random instance performance comparing test


In our project for one task we used static Random instance for random numbers generation goal. After Java 7 release new ThreadLocalRandom class appeared for generating random numbers.

From spec:

When applicable, use of ThreadLocalRandom rather than shared Random objects in concurrent programs will typically encounter much less overhead and contention. Use of ThreadLocalRandom is particularly appropriate when multiple tasks (for example, each a ForkJoinTask) use random numbers in parallel in thread pools.

and also:

When all usages are of this form, it is never possible to accidently share a ThreadLocalRandom across multiple threads.

So I've made my little test:

public class ThreadLocalRandomTest {

private static final int THREAD_COUNT = 100;
private static final int GENERATED_NUMBER_COUNT = 1000;
private static final int INT_RIGHT_BORDER = 5000;
private static final int EXPERIMENTS_COUNT = 5000;

public static void main(String[] args) throws InterruptedException {
    System.out.println("Number of threads: " + THREAD_COUNT);
    System.out.println("Length of generated numbers chain for each thread: " + GENERATED_NUMBER_COUNT);
    System.out.println("Right border integer: " + INT_RIGHT_BORDER);
    System.out.println("Count of experiments: " + EXPERIMENTS_COUNT);

    int repeats = 0;
    int workingTime = 0;
    long startTime = 0;
    long endTime = 0;

    for (int i = 0; i < EXPERIMENTS_COUNT; i++) {
        startTime = System.currentTimeMillis();
        repeats += calculateRepeatsForSharedRandom();
        endTime = System.currentTimeMillis();
        workingTime += endTime - startTime;
    }
    System.out.println("Average repeats for shared Random instance: " + repeats / EXPERIMENTS_COUNT
            + ". Average working time: " + workingTime / EXPERIMENTS_COUNT + " ms.");

    repeats = 0;
    workingTime = 0;
    for (int i = 0; i < EXPERIMENTS_COUNT; i++) {
        startTime = System.currentTimeMillis();
        repeats += calculateRepeatsForTheadLocalRandom();
        endTime = System.currentTimeMillis();
        workingTime += endTime - startTime;
    }
    System.out.println("Average repeats for ThreadLocalRandom: " + repeats / EXPERIMENTS_COUNT
            + ". Average working time: " + workingTime / EXPERIMENTS_COUNT + " ms.");
}

private static int calculateRepeatsForSharedRandom() throws InterruptedException {
    final Random rand = new Random();
    final Map<Integer, Integer> counts = new HashMap<>();

    for (int i = 0; i < THREAD_COUNT; i++) {
        Thread thread = new Thread() {
            @Override
            public void run() {

                for (int j = 0; j < GENERATED_NUMBER_COUNT; j++) {
                    int random = rand.nextInt(INT_RIGHT_BORDER);
                    if (!counts.containsKey(random)) {
                        counts.put(random, 0);
                    }
                    counts.put(random, counts.get(random) + 1);
                }
            }
        };
        thread.start();
        thread.join();
    }

    int repeats = 0;
    for (Integer value : counts.values()) {
        if (value > 1) {
            repeats += value;
        }
    }

    return repeats;
}

private static int calculateRepeatsForTheadLocalRandom() throws InterruptedException {
    final Map<Integer, Integer> counts = new HashMap<>();

    for (int i = 0; i < THREAD_COUNT; i++) {
        Thread thread = new Thread() {
            @Override
            public void run() {

                for (int j = 0; j < GENERATED_NUMBER_COUNT; j++) {
                    int random = ThreadLocalRandom.current().nextInt(INT_RIGHT_BORDER);
                    if (!counts.containsKey(random)) {
                        counts.put(random, 0);
                    }
                    counts.put(random, counts.get(random) + 1);
                }
            }
        };
        thread.start();
        thread.join();
    }

    int repeats = 0;
    for (Integer value : counts.values()) {
        if (value > 1) {
            repeats += value;
        }
    }

    return repeats;
}

}

I've also added test for non-shared Random and got next results:

Number of threads: 100
Length of generated numbers chain for each thread: 100
Right border integer: 5000
Count of experiments: 10000
Average repeats for non-shared Random instance: 8646. Average working time: 13 ms.
Average repeats for shared Random instance: 8646. Average working time: 13 ms.
Average repeats for ThreadLocalRandom: 8646. Average working time: 13 ms.

To me it's little strange, as I expected at least speed increasing when using ThreadLocalRandom comparing to shared Random instance, but see no difference at all.

Can someone explain why it works that way, maybe I haven't done testing properly. Thank you!


Solution

  • You're not running anything in parallel because you're waiting for each thread to finish immediately after starting it. You need a waiting loop outside the loop that starts the threads:

    List<Thread> threads = new ArrayList<Thread>();
    for (int i = 0; i < THREAD_COUNT; i++) {
        Thread thread = new Thread() {
            @Override
            public void run() {
    
                for (int j = 0; j < GENERATED_NUMBER_COUNT; j++) {
                    int random = rand.nextInt(INT_RIGHT_BORDER);
                    if (!counts.containsKey(random)) {
                        counts.put(random, 0);
                    }
                    counts.put(random, counts.get(random) + 1);
                }
            }
        };
        threads.add(thread);
        thread.start();
    }
    
    for (Thread thread: threads) {
        thread.join();
    }