Secure random number generation in Java means producing values that an attacker cannot predict, even after seeing earlier output, and the JDK class for it is java.security.SecureRandom. It draws its seed from the operating system and runs a cryptographically strong generator, whereas java.util.Random follows a formula that anyone can replay once they know a few outputs.
We use SecureRandom for anything that protects an account or data, such as password reset tokens, session and API keys, one-time login codes, salts for password hashing, and keys and IVs for encryption. For games, simulations and shuffling a playlist, a regular generator is faster and good enough.
The following example creates one SecureRandom and generates a number in a range, a login code and a URL-safe token.
SecureRandom random = new SecureRandom();
int diceRoll = random.nextInt(1, 7); // a value from 1 to 6
int loginCode = random.nextInt(100_000, 1_000_000); // a 6-digit code, e.g. 482915
byte[] secret = new byte[32];
random.nextBytes(secret); // fills 32 unpredictable bytes
String token = Base64.getUrlEncoder().withoutPadding().encodeToString(secret);
int tokenLength = token.length(); // 43
Notice that we create the generator with the plain constructor and no algorithm name, which is the recommended default on Java 25. We compare SecureRandom with Random, generate numbers, tokens and codes, choose between the available algorithms including DRBG and getInstanceStrong(), and look at the seeding and threading mistakes that make secure random numbers predictable.
1. SecureRandom vs Random
The class java.util.Random is a linear congruential generator with a 48-bit seed. Every value comes from the previous internal state through a fixed formula, so two generators with the same seed return the same sequence. Researchers have shown that two consecutive nextInt() outputs are enough to rebuild the state and compute every future value.
Random first = new Random(42);
Random second = new Random(42);
boolean sameValues = first.nextInt() == second.nextInt(); // true, the seed decides everything
Say an app builds password reset links with Random. An attacker requests a reset for their own account, reads the token from the email, computes the generator state, and predicts the next token, which the app sends to the victim a minute later. With SecureRandom, earlier tokens reveal nothing about later ones.

Java has several generators, and the right one depends on whether the values must stay secret. The random number generators article covers the non-secure ones in detail.
| Generator | Predictable from output | Typical use |
|---|---|---|
| Math.random(), new Random() | Yes | Legacy code, simple demos |
| ThreadLocalRandom.current() | Yes | Fast values in multi-threaded code, such as jitter for retries |
| RandomGenerator.of(“L64X128MixRandom”) (Java 17) | Yes | Simulations and tests that need a better algorithm |
| new SecureRandom() | No | Tokens, keys, salts, IVs, one-time codes |
| SecureRandom.getInstanceStrong() | No | Long-term keys where the strongest configured source is required |
2. Secure Random Number Generation in Java with nextInt() and nextBytes()
SecureRandom extends Random and, since Java 17, implements the RandomGenerator interface. So it has the same methods as the other generators, including nextInt(origin, bound) for a range, where the origin is included and the bound is excluded.
SecureRandom random = new SecureRandom();
int percent = random.nextInt(101); // 0 to 100
int fromRange = random.nextInt(18, 66); // 18 to 65
long bigValue = random.nextLong(); // any long value
double fraction = random.nextDouble(); // 0.0 inclusive to 1.0 exclusive
boolean coin = random.nextBoolean(); // true or false
byte[] salt = new byte[16];
random.nextBytes(salt); // 16 random bytes for a password hash
The stream methods ints(), longs() and doubles() return many values at once. For example, a raffle picks six different ticket numbers between 1 and 49.
SecureRandom random = new SecureRandom();
List<Integer> winners = random.ints(1, 50)
.distinct()
.limit(6)
.sorted()
.boxed()
.toList();
int winnerCount = winners.size(); // 6
For the range rules and more examples, see random numbers in a range. All of them work the same way on a SecureRandom instance.
3. Generating Secure Tokens and Codes
Most security code needs random text, not random numbers. A token is a string that proves the holder received it from us, such as a password reset link, an email confirmation link or an API key, so it must be long enough that guessing is hopeless.
We generate tokens from at least 32 random bytes and encode them, instead of building them from nextInt() calls or a timestamp. Thirty-two bytes give 256 bits of randomness, and the encoding only changes how the bytes look.
static String newResetToken(SecureRandom random) {
byte[] bytes = new byte[32];
random.nextBytes(bytes);
return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
}
static String newApiKey(SecureRandom random) {
byte[] bytes = new byte[32];
random.nextBytes(bytes);
return "key_" + HexFormat.of().formatHex(bytes);
}
SecureRandom random = new SecureRandom();
String resetToken = newResetToken(random); // e.g. "Xq3v0bK1...", 43 characters
int resetLength = resetToken.length(); // 43
String apiKey = newApiKey(random); // "key_" plus 64 hex digits
int apiKeyLength = apiKey.length(); // 68
Base64 URL-safe encoding uses letters, digits, – and _, so the token fits in a URL without escaping, and Base64 encoding explains the alphabets. The HexFormat class, added in Java 17, prints bytes as hex digits, which is common for API keys.
Codes that people type need a different shape. A one-time login code has six digits, so we format it with leading zeros, and a voucher code uses an alphabet without characters that look alike, such as 0 and O.
static String randomCode(SecureRandom random, int length) {
String alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
StringBuilder code = new StringBuilder(length);
for (int i = 0; i < length; i++) {
code.append(alphabet.charAt(random.nextInt(alphabet.length())));
}
return code.toString();
}
SecureRandom random = new SecureRandom();
String otp = String.format("%06d", random.nextInt(1_000_000)); // e.g. "004817"
int otpLength = otp.length(); // 6
String voucher = randomCode(random, 10); // e.g. "K7QM2XHP9D"
int voucherLength = voucher.length(); // 10
A six-digit code has only one million values, so it is safe only with a short expiry and a limit on attempts. We store tokens the same way as passwords, as a hash in the database, so a leaked backup does not hand out working reset links, and secure password hashing shows the hashing side.
4. Choosing a SecureRandom Algorithm
A SecureRandom object delegates to an algorithm from a security provider. The plain constructor picks the first algorithm in the platform’s preference list, and on Java 25 that list gives different defaults per operating system.
| Algorithm | Source of bytes | Default for |
|---|---|---|
| NativePRNG | nextBytes() reads /dev/urandom, generateSeed() reads /dev/random | new SecureRandom() on Linux and macOS |
| DRBG | A NIST SP 800-90A generator seeded by the OS (Java 9, JEP 273) | new SecureRandom() on Windows |
| NativePRNGBlocking | /dev/random for both methods | getInstanceStrong() on Linux and macOS |
| NativePRNGNonBlocking | /dev/urandom for both methods | None, available on request |
| SHA1PRNG | A SHA-1 based generator, seeded once | None, kept for old code |
SecureRandom platformDefault = new SecureRandom();
String algorithm = platformDefault.getAlgorithm(); // "NativePRNG" on Linux and macOS, "DRBG" on Windows
SecureRandom strong = SecureRandom.getInstanceStrong();
String strongAlgorithm = strong.getAlgorithm(); // "NativePRNGBlocking" on Linux
Many older tutorials tell readers to always request SHA1PRNG from the SUN provider. The advice is outdated, because the platform default is a stronger choice, and code that names an algorithm fails with NoSuchAlgorithmException on a JVM that lacks it.
4.1. DRBG with Explicit Parameters
The DRBG algorithm, added in Java 9, is the one to pick when a security policy asks for a specific strength or for prediction resistance. We pass DrbgParameters with the strength in bits, the capability and an optional personalization string that mixes an app-specific value into the seed.
SecureRandom drbg = SecureRandom.getInstance("DRBG",
DrbgParameters.instantiation(256, DrbgParameters.Capability.PR_AND_RESEED,
"invoice-service".getBytes(StandardCharsets.UTF_8)));
String config = drbg.toString(); // "Hash_DRBG,SHA-256,256,pr_and_reseed"
The capability PR_AND_RESEED makes the generator fetch fresh entropy for every request and allows manual reseed() calls. Without parameters, getInstance(“DRBG”) returns Hash_DRBG with SHA-256 at 128-bit strength, which is enough for tokens and session IDs.
4.2. When to Use getInstanceStrong()
The getInstanceStrong() method returns the first algorithm in the securerandom.strongAlgorithms property of the java.security file, which is NativePRNGBlocking on Linux. That algorithm reads /dev/random, and on Linux kernels before 5.6, or very early after boot on newer kernels, a read can block until the kernel has collected enough entropy.
A blocked read during startup shows up as a service that hangs for seconds or minutes in a fresh container or VM. So we use getInstanceStrong() only for rare, long-lived secrets, such as a signing key generated once, and keep new SecureRandom() for request-time values such as tokens and session IDs.
5. Seeding and Reusing a SecureRandom
A SecureRandom seeds itself from the operating system on first use, so we never need to call setSeed(). For most algorithms, setSeed() adds our bytes to the existing state and does not replace it, so a constant seed does no harm there.
SHA1PRNG is the exception. When we seed it before the first call, it uses only our seed, and every instance seeded with the same value returns the same numbers.
SecureRandom legacyA = SecureRandom.getInstance("SHA1PRNG");
legacyA.setSeed(42L);
SecureRandom legacyB = SecureRandom.getInstance("SHA1PRNG");
legacyB.setSeed(42L);
boolean predictable = legacyA.nextInt() == legacyB.nextInt(); // true, only our seed is used
SecureRandom nativeA = new SecureRandom();
nativeA.setSeed(42L);
SecureRandom nativeB = new SecureRandom();
nativeB.setSeed(42L);
boolean independent = nativeA.nextInt() != nativeB.nextInt(); // true, the OS seed is still mixed in
SecureRandom is thread-safe, and creating one costs more than calling it, because the first call gathers seed material. So we create one instance per app or per service class, keep it in a static final field, and share it across threads, as described in random generators and concurrency.
class TokenService {
private static final SecureRandom RANDOM = new SecureRandom();
String newSessionId() {
byte[] bytes = new byte[32];
RANDOM.nextBytes(bytes);
return HexFormat.of().formatHex(bytes);
}
}
TokenService service = new TokenService();
int sessionIdLength = service.newSessionId().length(); // 64
Older advice to create a new instance every few minutes to reseed it is not needed. The native algorithms read fresh bytes from the OS on every call, and a DRBG instance reseeds itself and accepts manual reseed() calls.
6. SecureRandom FAQs
Startup delays in containers and token formats come up whenever a team adopts SecureRandom.
6.1. Is SecureRandom Thread-Safe?
Yes. SecureRandom objects are safe for use by multiple concurrent threads. All algorithms of the JDK’s SUN provider declare themselves thread-safe, so concurrent calls run without a lock, and for a third-party provider without that declaration, SecureRandom synchronizes the calls.
6.2. Does SecureRandom Block?
The default new SecureRandom() on Linux reads /dev/urandom for its values, which does not block once the system has booted. Blocking can happen with getInstanceStrong() or generateSeed(), which read /dev/random, mostly on old kernels or right after a VM starts.
6.3. Is UUID.randomUUID() Secure?
Yes, for identifiers. The method uses a cryptographically strong generator and fills 122 of the 128 bits with random data, so the IDs are unpredictable. For secret tokens we prefer 32 random bytes, which give 256 bits and a shorter string, while Java UUIDs remain a good fit for public IDs.
6.4. Is Math.random() Secure?
No. Math.random() uses a shared java.util.Random instance, so its values are predictable from earlier output. We use it only where predictability does no harm, such as a random greeting on a page.
6.5. Is SHA1PRNG Still Safe to Use?
No, not for new code. SHA1PRNG is still available in Java 25, but only for compatibility. Its output is predictable when it is seeded explicitly, as section 5 shows, and it is not the default on any platform, so we use new SecureRandom() or DRBG instead.
7. Conclusion
SecureRandom produces random numbers and bytes that an attacker cannot predict, which Random, Math.random() and ThreadLocalRandom cannot promise. For tokens, keys, salts and one-time codes, we create one new SecureRandom(), share it across threads, and turn 32 random bytes into a Base64 or hex string.
The platform default is NativePRNG on Linux and macOS and DRBG on Windows. We request DRBG with DrbgParameters when a policy needs a specific strength, use getInstanceStrong() only for rare long-lived keys because it can block, and never seed SHA1PRNG with a fixed value.
8. References
- SecureRandom Javadoc (Java 25)
- DrbgParameters Javadoc (Java 25)
- JDK Providers Documentation, SUN Provider (Java 25)
- JEP 273 DRBG-Based SecureRandom Implementations
- JEP 356 Enhanced Pseudo-Random Number Generators
Happy Learning !!