Question

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!

Was it helpful?

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();
}

OTHER TIPS

Your testing code is flawed for one. The bane of benchmarkers everywhere.

thread.start();
thread.join();

why not save LOCs and write

thread.run();

the outcome is the same.

EDIT: If you don't realize the outcome from the above, it means that you're running single threaded tests, there's no multithreading going on.

Maybe it would be easier to just have a look at what actually happens. Here is the source for ThreadLocal.get() which is also called for the ThreadLocalRandom.current().

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null)
            return (T)e.value;
    }
    return setInitialValue();
}

Where ThreadLocalMap is a specialized HashMap-like implementation with optimizations.

So what basically happens is that ThreadLocal holds a map Thread->Object - or in this case Thread->Random - which is then looked up and either returned or created. As this is nothing 'magical', the timing will be equal to a HashMap-lookup + the initial creation overhead of the actual Object to be returned. Since a HashMap lookup (in this optimized case) is linear, the cost for a lookup is k, where k is the calculation cost of the hash function.

So you can make some assumptions:

  • ThreadLocal will be faster than creating the object each time in each Runnable, unless the creation cost is much smaller than k. So looking up Random is a good thing, putting an int inside might not be so smart.

  • ThreadLocal will be better than using your own HashMap, as such a generic implementation can be assumed to be equal to k or worse.

  • ThreadLocal will be slower than using any lookup with a cost < k. Example: store everything in an array first, then do myRandoms[threadID].
    But then this assumes that you know which threads will be processing your work in the first place, so this isn't a real candidate for ThreadLocal anyways.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top