BLOG POSTS
Java Random – Generating Random Numbers

Java Random – Generating Random Numbers

Java’s random number generation capabilities are essential for everything from testing and simulations to cryptographic applications and game development. Whether you’re building a load balancer that needs to distribute traffic randomly, implementing a cache eviction strategy, or creating secure tokens for API authentication, understanding Java’s random number generators will save you from subtle bugs and performance issues. This guide walks through the Random class, SecureRandom for cryptographic use cases, ThreadLocalRandom for concurrent applications, and covers the gotchas that can bite you in production environments.

How Java Random Number Generation Works

Java provides several classes for generating random numbers, each with different characteristics and use cases. The basic java.util.Random class uses a linear congruential generator (LCG) algorithm, which is fast but not cryptographically secure. Under the hood, it maintains an internal seed value that gets updated with each call to generate the next pseudorandom number.

The algorithm follows this pattern:

next_seed = (seed * multiplier + increment) % modulus

This means if you know the seed and the algorithm parameters, you can predict the entire sequence. That’s why Random is fine for simulations and testing but terrible for security-sensitive applications.

For concurrent applications, Java 7 introduced ThreadLocalRandom, which maintains separate generator instances per thread to avoid contention. For cryptographic purposes, SecureRandom uses entropy sources from the operating system to generate unpredictable numbers.

Step-by-Step Implementation Guide

Basic Random Usage

Here’s how to get started with the standard Random class:

import java.util.Random;

public class BasicRandomExample {
    public static void main(String[] args) {
        // Create with system-generated seed
        Random random = new Random();
        
        // Or create with specific seed for reproducible results
        Random seededRandom = new Random(12345L);
        
        // Generate different types of random numbers
        int randomInt = random.nextInt();           // Any int value
        int boundedInt = random.nextInt(100);       // 0 to 99
        long randomLong = random.nextLong();        // Any long value
        double randomDouble = random.nextDouble();   // 0.0 to 1.0
        boolean randomBoolean = random.nextBoolean(); // true or false
        
        // Generate random numbers in a range
        int min = 10, max = 50;
        int rangeInt = random.nextInt(max - min) + min; // 10 to 49
        
        System.out.println("Random int: " + randomInt);
        System.out.println("Bounded int: " + boundedInt);
        System.out.println("Range int: " + rangeInt);
    }
}

ThreadLocalRandom for Concurrent Applications

When you’re working in multithreaded environments, ThreadLocalRandom eliminates synchronization overhead:

import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.IntStream;

public class ConcurrentRandomExample {
    public static void main(String[] args) {
        // Generate random numbers in parallel streams
        IntStream.range(0, 1000000)
            .parallel()
            .forEach(i -> {
                // Each thread gets its own Random instance
                int random = ThreadLocalRandom.current().nextInt(1, 101);
                // Process the random number
                processValue(random);
            });
        
        // Generate ranges more elegantly
        int randomInRange = ThreadLocalRandom.current().nextInt(10, 50);
        double randomDouble = ThreadLocalRandom.current().nextDouble(0.0, 1.0);
        long randomLong = ThreadLocalRandom.current().nextLong(1000L, 9999L);
    }
    
    private static void processValue(int value) {
        // Your processing logic here
    }
}

SecureRandom for Cryptographic Applications

For generating secure tokens, session IDs, or cryptographic keys, use SecureRandom:

import java.security.SecureRandom;
import java.util.Base64;

public class SecureRandomExample {
    private static final SecureRandom secureRandom = new SecureRandom();
    
    public static String generateSessionToken() {
        byte[] randomBytes = new byte[32];
        secureRandom.nextBytes(randomBytes);
        return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes);
    }
    
    public static String generateApiKey() {
        byte[] keyBytes = new byte[24];
        secureRandom.nextBytes(keyBytes);
        return Base64.getEncoder().encodeToString(keyBytes);
    }
    
    public static void main(String[] args) {
        System.out.println("Session token: " + generateSessionToken());
        System.out.println("API key: " + generateApiKey());
        
        // Generate secure random numbers
        int secureInt = secureRandom.nextInt(1000000);
        System.out.println("Secure random int: " + secureInt);
    }
}

Real-World Examples and Use Cases

Load Balancing with Weighted Random Selection

Here’s a practical example of using random numbers for server load balancing:

import java.util.*;
import java.util.concurrent.ThreadLocalRandom;

public class WeightedLoadBalancer {
    private final List<Server> servers;
    private final int totalWeight;
    
    public WeightedLoadBalancer(List<Server> servers) {
        this.servers = new ArrayList<>(servers);
        this.totalWeight = servers.stream().mapToInt(Server::getWeight).sum();
    }
    
    public Server selectServer() {
        int randomWeight = ThreadLocalRandom.current().nextInt(totalWeight);
        int currentWeight = 0;
        
        for (Server server : servers) {
            currentWeight += server.getWeight();
            if (randomWeight < currentWeight) {
                return server;
            }
        }
        return servers.get(servers.size() - 1); // Fallback
    }
    
    static class Server {
        private final String name;
        private final int weight;
        
        public Server(String name, int weight) {
            this.name = name;
            this.weight = weight;
        }
        
        public int getWeight() { return weight; }
        public String getName() { return name; }
    }
    
    public static void main(String[] args) {
        List<Server> servers = Arrays.asList(
            new Server("server-1", 30),
            new Server("server-2", 50),
            new Server("server-3", 20)  
        );
        
        WeightedLoadBalancer balancer = new WeightedLoadBalancer(servers);
        
        // Test distribution
        Map<String, Integer> distribution = new HashMap<>();
        for (int i = 0; i < 10000; i++) {
            Server selected = balancer.selectServer();
            distribution.merge(selected.getName(), 1, Integer::sum);
        }
        
        distribution.forEach((server, count) -> 
            System.out.println(server + ": " + count + " selections"));
    }
}

Cache Eviction with Random Sampling

Random sampling is useful for implementing cache eviction policies:

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;

public class RandomSamplingCache<K, V> {
    private final Map<K, CacheEntry<V>> cache = new ConcurrentHashMap<>();
    private final int maxSize;
    private final int sampleSize;
    
    public RandomSamplingCache(int maxSize, int sampleSize) {
        this.maxSize = maxSize;
        this.sampleSize = sampleSize;
    }
    
    public void put(K key, V value) {
        if (cache.size() >= maxSize) {
            evictRandomSample();
        }
        cache.put(key, new CacheEntry<>(value, System.currentTimeMillis()));
    }
    
    private void evictRandomSample() {
        List<K> keys = new ArrayList<>(cache.keySet());
        Collections.shuffle(keys, ThreadLocalRandom.current());
        
        // Find oldest entry in random sample
        K oldestKey = keys.stream()
            .limit(sampleSize)
            .min(Comparator.comparing(k -> cache.get(k).timestamp))
            .orElse(keys.get(0));
            
        cache.remove(oldestKey);
    }
    
    public V get(K key) {
        CacheEntry<V> entry = cache.get(key);
        return entry != null ? entry.value : null;
    }
    
    private static class CacheEntry<V> {
        final V value;
        final long timestamp;
        
        CacheEntry(V value, long timestamp) {
            this.value = value;
            this.timestamp = timestamp;
        }
    }
}

Performance Comparison and Benchmarks

Different random number generators have significantly different performance characteristics:

Generator Single Thread (ops/sec) Multi Thread (ops/sec) Memory Usage Use Case
Random ~50M ~8M (contended) Low Simple applications, testing
ThreadLocalRandom ~55M ~200M (scales) Medium Concurrent applications
SecureRandom ~500K ~2M Low Cryptographic applications
SplittableRandom ~60M ~250M Medium Parallel streams, fork-join

Here’s a benchmarking example you can run on your VPS or dedicated server:

import java.security.SecureRandom;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;

public class RandomPerformanceBenchmark {
    private static final int ITERATIONS = 10_000_000;
    
    public static void main(String[] args) {
        benchmarkRandom();
        benchmarkThreadLocalRandom();
        benchmarkSecureRandom();
    }
    
    private static void benchmarkRandom() {
        Random random = new Random();
        long start = System.nanoTime();
        
        for (int i = 0; i < ITERATIONS; i++) {
            random.nextInt(1000);
        }
        
        long duration = System.nanoTime() - start;
        System.out.printf("Random: %.2f M ops/sec%n", 
            ITERATIONS / (duration / 1_000_000_000.0) / 1_000_000);
    }
    
    private static void benchmarkThreadLocalRandom() {
        long start = System.nanoTime();
        
        for (int i = 0; i < ITERATIONS; i++) {
            ThreadLocalRandom.current().nextInt(1000);
        }
        
        long duration = System.nanoTime() - start;
        System.out.printf("ThreadLocalRandom: %.2f M ops/sec%n", 
            ITERATIONS / (duration / 1_000_000_000.0) / 1_000_000);
    }
    
    private static void benchmarkSecureRandom() {
        SecureRandom secureRandom = new SecureRandom();
        long start = System.nanoTime();
        
        for (int i = 0; i < ITERATIONS / 100; i++) { // Fewer iterations
            secureRandom.nextInt(1000);
        }
        
        long duration = System.nanoTime() - start;
        System.out.printf("SecureRandom: %.2f K ops/sec%n", 
            (ITERATIONS / 100) / (duration / 1_000_000_000.0) / 1_000);
    }
}

Common Pitfalls and Best Practices

Avoid These Common Mistakes

  • Creating new Random instances frequently: This is expensive and can lead to poor randomness if instances are created close in time with similar seeds
  • Using Random in multithreaded code: The synchronization overhead kills performance and can create contention hotspots
  • Using Random for security purposes: It’s predictable if an attacker knows the seed or can observe enough outputs
  • Not handling SecureRandom initialization properly: The first call can be very slow as it gathers entropy
  • Modulo bias with bounded ranges: Using random.nextInt() % n creates bias toward smaller numbers

Best Practices

Follow these guidelines for robust random number generation:

public class RandomBestPractices {
    // Singleton pattern for application-wide random instances
    private static final Random RANDOM = new Random();
    private static final SecureRandom SECURE_RANDOM = new SecureRandom();
    
    // Pre-generate SecureRandom to avoid first-call delay
    static {
        SECURE_RANDOM.nextBytes(new byte[1]);
    }
    
    // Correct way to generate random in range (avoid modulo bias)
    public static int randomInRange(int min, int max) {
        return ThreadLocalRandom.current().nextInt(min, max);
    }
    
    // Secure token generation with proper length
    public static String generateSecureToken(int byteLength) {
        byte[] bytes = new byte[byteLength];
        SECURE_RANDOM.nextBytes(bytes);
        return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
    }
    
    // Reservoir sampling for random selection from streams
    public static <T> List<T> reservoirSample(Stream<T> stream, int sampleSize) {
        List<T> reservoir = new ArrayList<>(sampleSize);
        AtomicInteger count = new AtomicInteger(0);
        
        stream.forEach(item -> {
            int index = count.incrementAndGet();
            if (index <= sampleSize) {
                reservoir.add(item);
            } else {
                int randomIndex = ThreadLocalRandom.current().nextInt(index);
                if (randomIndex < sampleSize) {
                    reservoir.set(randomIndex, item);
                }
            }
        });
        
        return reservoir;
    }
}

Testing Random Code

Always use seeded generators for unit tests to ensure reproducible results:

public class RandomTestExample {
    @Test
    public void testRandomBehavior() {
        // Use fixed seed for reproducible tests
        Random testRandom = new Random(42L);
        
        List<Integer> results = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            results.add(testRandom.nextInt(100));
        }
        
        // These results will always be the same
        List<Integer> expected = Arrays.asList(33, 51, 13, 81, 28, 0, 36, 98, 16, 7);
        assertEquals(expected, results);
    }
    
    @Test 
    public void testRandomDistribution() {
        Random random = new Random(12345L);
        Map<Integer, Integer> distribution = new HashMap<>();
        
        // Generate many samples
        for (int i = 0; i < 100000; i++) {
            int value = random.nextInt(10);
            distribution.merge(value, 1, Integer::sum);
        }
        
        // Check distribution is roughly uniform
        for (int count : distribution.values()) {
            assertTrue("Distribution should be roughly uniform", 
                Math.abs(count - 10000) < 1000);
        }
    }
}

Advanced Random Generation Techniques

Custom Probability Distributions

Sometimes you need non-uniform distributions. Here’s how to implement common patterns:

import java.util.concurrent.ThreadLocalRandom;

public class CustomDistributions {
    
    // Generate normal distribution using Box-Muller transform
    public static double nextGaussian(double mean, double stdDev) {
        return ThreadLocalRandom.current().nextGaussian() * stdDev + mean;
    }
    
    // Generate exponential distribution
    public static double nextExponential(double lambda) {
        return -Math.log(1.0 - ThreadLocalRandom.current().nextDouble()) / lambda;
    }
    
    // Generate from custom discrete distribution
    public static <T> T nextWeighted(Map<T, Double> weights) {
        double totalWeight = weights.values().stream().mapToDouble(Double::doubleValue).sum();
        double random = ThreadLocalRandom.current().nextDouble() * totalWeight;
        
        double currentWeight = 0;
        for (Map.Entry<T, Double> entry : weights.entrySet()) {
            currentWeight += entry.getValue();
            if (random <= currentWeight) {
                return entry.getKey();
            }
        }
        throw new IllegalStateException("Should not reach here");
    }
    
    public static void main(String[] args) {
        // Test normal distribution
        for (int i = 0; i < 10; i++) {
            System.out.printf("Gaussian: %.2f%n", nextGaussian(100, 15));
        }
        
        // Test weighted selection
        Map<String, Double> serverWeights = Map.of(
            "server-1", 0.5,
            "server-2", 0.3,
            "server-3", 0.2
        );
        
        for (int i = 0; i < 5; i++) {
            System.out.println("Selected: " + nextWeighted(serverWeights));
        }
    }
}

Random Data Generation for Testing

Generate realistic test data using random patterns:

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.ThreadLocalRandom;

public class TestDataGenerator {
    private static final String[] FIRST_NAMES = {"John", "Jane", "Mike", "Sarah", "David", "Emma"};
    private static final String[] LAST_NAMES = {"Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia"};
    private static final String[] DOMAINS = {"gmail.com", "yahoo.com", "outlook.com", "example.org"};
    
    public static class User {
        public final String firstName;
        public final String lastName;
        public final String email;
        public final int age;
        public final LocalDateTime createdAt;
        
        public User(String firstName, String lastName, String email, int age, LocalDateTime createdAt) {
            this.firstName = firstName;
            this.lastName = lastName;
            this.email = email;
            this.age = age;
            this.createdAt = createdAt;
        }
        
        @Override
        public String toString() {
            return String.format("%s %s (%s), age %d, created %s", 
                firstName, lastName, email, age, 
                createdAt.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
        }
    }
    
    public static User generateRandomUser() {
        ThreadLocalRandom random = ThreadLocalRandom.current();
        
        String firstName = FIRST_NAMES[random.nextInt(FIRST_NAMES.length)];
        String lastName = LAST_NAMES[random.nextInt(LAST_NAMES.length)];
        String email = firstName.toLowerCase() + "." + lastName.toLowerCase() + 
                      random.nextInt(1000) + "@" + 
                      DOMAINS[random.nextInt(DOMAINS.length)];
        int age = random.nextInt(18, 80);
        LocalDateTime createdAt = LocalDateTime.now().minusDays(random.nextInt(365));
        
        return new User(firstName, lastName, email, age, createdAt);
    }
    
    public static void main(String[] args) {
        System.out.println("Generated test users:");
        for (int i = 0; i < 5; i++) {
            System.out.println(generateRandomUser());
        }
    }
}

Understanding Java’s random number generation capabilities is crucial for building robust applications. Whether you’re implementing load balancing algorithms, generating test data, or securing user sessions, choosing the right random number generator and avoiding common pitfalls will make your applications more reliable and performant. The key is matching the generator to your use case: Random for simple needs, ThreadLocalRandom for concurrent applications, and SecureRandom when security matters.

For more detailed information about Java’s random number generators, check out the official Java Random documentation and the SecureRandom specification.



This article incorporates information and material from various online sources. We acknowledge and appreciate the work of all original authors, publishers, and websites. While every effort has been made to appropriately credit the source material, any unintentional oversight or omission does not constitute a copyright infringement. All trademarks, logos, and images mentioned are the property of their respective owners. If you believe that any content used in this article infringes upon your copyright, please contact us immediately for review and prompt action.

This article is intended for informational and educational purposes only and does not infringe on the rights of the copyright owners. If any copyrighted material has been used without proper credit or in violation of copyright laws, it is unintentional and we will rectify it promptly upon notification. Please note that the republishing, redistribution, or reproduction of part or all of the contents in any form is prohibited without express written permission from the author and website owner. For permissions or further inquiries, please contact us.

Leave a reply

Your email address will not be published. Required fields are marked