diff --git a/src/java.base/share/classes/java/util/UUID.java b/src/java.base/share/classes/java/util/UUID.java index 2f5ad22653d1..99064c373502 100644 --- a/src/java.base/share/classes/java/util/UUID.java +++ b/src/java.base/share/classes/java/util/UUID.java @@ -25,10 +25,15 @@ package java.util; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.util.concurrent.locks.StampedLock; import java.security.*; import jdk.internal.access.JavaLangAccess; import jdk.internal.access.SharedSecrets; +import jdk.internal.util.random.RandomSupport; +import jdk.internal.util.ByteArray; /** * A class that represents an immutable universally unique identifier (UUID). @@ -99,9 +104,141 @@ public final class UUID implements java.io.Serializable, Comparable { /* * The random number generator used by this class to create random * based UUIDs. In a holder class to defer initialization until needed. + *

+ * The implementation buffers the reads to avoid bashing SecureRandom with + * small requests. It also maintains a SecureRandom per buffer to alleviate + * scalability bottlenecks when reading from a single (synchronized) SecureRandom. */ - private static class Holder { - static final SecureRandom numberGenerator = new SecureRandom(); + private static final class RandomUUID { + static final int BUFS_COUNT; + static final Buffer[] BUFS; + + private static final int roundPowerOfTwo(int x) { + int n = -1 >>> Integer.numberOfLeadingZeros(x - 1); + return (n < 0) ? 1 : (n + 1); + } + + static { + BUFS_COUNT = roundPowerOfTwo(Runtime.getRuntime().availableProcessors()); + BUFS = new Buffer[BUFS_COUNT]; + } + + public static UUID next() { + // We want to hit the same buffer from the same thread for several reasons: + // - in best case, make thread poll from the single random stream; + // - avoid instantiating too many buffers when only a few calling threads; + // - make sure the buffers stay hot in the local caches; + // - minimize coherence traffic for cursor updates + // + // Without recording the buffer index in the thread itself, the good option is to use + // the thread ID scrambled with Murmur hash, which results in good bit entropy. + + long h = RandomSupport.mixMurmur64(Thread.currentThread().threadId()); + int idx = (int)(h & (BUFS_COUNT - 1)); + Buffer current = BUFS[idx]; + if (current == null) { + // Create a new buffer and install it. + // On initial contention, some buffers may be lost, but this is not + // a problem for correctness, or for steady-state performance. + current = new Buffer(); + BUFS[idx] = current; + } + return current.next(); + } + + // Buffer random reads. This allows batching the SecureRandom provider requests. + // Current implementation targets the 4K buffer size, which balances the initialization + // costs, memory footprint and cache pressure. + @jdk.internal.vm.annotation.Contended + static final class Buffer { + static final int UUID_CHUNK = 16; + static final int UUID_COUNT = 256; + static final int BUF_SIZE; + + static final VarHandle VH_POS; + static { + try { + VH_POS = MethodHandles.lookup().findVarHandle(Buffer.class, "pos", int.class); + BUF_SIZE = UUID_CHUNK * UUID_COUNT; + } catch (Exception e) { + throw new InternalError(e); + } + } + + final SecureRandom random; + final StampedLock lock; + final byte[] buf; + int pos; + + public Buffer() { + this.random = new SecureRandom(); + this.lock = new StampedLock(); + this.buf = new byte[BUF_SIZE]; + this.pos = BUF_SIZE; // trigger re-creation on first use + } + + private static UUID fromRandom(long msb, long lsb) { + // set version to 3 + msb = (msb & (0xFFFF_FFFF_FFFF_0FFFL)) | 0x0000_0000_0000_4000L; + // set variant to IETF + lsb = (lsb & (0x3FFF_FFFF_FFFF_FFFFL)) | 0x8000_0000_0000_0000L; + return new UUID(msb, lsb); + } + + public UUID next() { + long stamp = lock.tryOptimisticRead(); + try { + // Optimistic path: optimistic locking succeeded. + // Try to pull the UUID from the current buffer at current position. + if (stamp != 0) { + int p = (int)VH_POS.getAndAdd(this, UUID_CHUNK); + if (p < BUF_SIZE) { + long msb = ByteArray.getLong(buf, p); + long lsb = ByteArray.getLong(buf, p + 8); + if (lock.validate(stamp)) { + // Success: there were no buffer changes. Construct the UUID. + return fromRandom(msb, lsb); + } + } + } + + // Semi-pessimistic path: either the buffer was depleted, or optimistic locking + // failed. Either way, we need to take the exclusive lock and try again. + stamp = lock.tryConvertToWriteLock(stamp); + if (stamp == 0L) { + stamp = lock.writeLock(); + } + + // See if some other thread had already replenished the buffer. + // Pull the UUID from there then. We are still holding the write lock, so + // buffer is guaranteed to not change under our feet. + if ((int)VH_POS.get(this) > 0) { + int p = (int)VH_POS.getAndAdd(this, UUID_CHUNK); + if (p < BUF_SIZE) { + long msb = ByteArray.getLong(buf, p); + long lsb = ByteArray.getLong(buf, p + 8); + return fromRandom(msb, lsb); + } + } + + // Pessimistic path: buffer requires replenishment. + // Recreate it from the provided random. + random.nextBytes(buf); + + // Take the UUID from new buffer. We are still under write lock, + // so we know we are the only thread here. + VH_POS.set(this, UUID_CHUNK); + + long msb = ByteArray.getLong(buf, 0); + long lsb = ByteArray.getLong(buf, 8); + return fromRandom(msb, lsb); + } finally { + if (StampedLock.isWriteLockStamp(stamp)) { + lock.unlockWrite(stamp); + } + } + } + } } // Constructors and Factories @@ -147,15 +284,7 @@ public UUID(long mostSigBits, long leastSigBits) { * @return A randomly generated {@code UUID} */ public static UUID randomUUID() { - SecureRandom ng = Holder.numberGenerator; - - byte[] randomBytes = new byte[16]; - ng.nextBytes(randomBytes); - randomBytes[6] &= 0x0f; /* clear version */ - randomBytes[6] |= 0x40; /* set to version 4 */ - randomBytes[8] &= 0x3f; /* clear variant */ - randomBytes[8] |= (byte) 0x80; /* set to IETF variant */ - return new UUID(randomBytes); + return RandomUUID.next(); } /** diff --git a/test/micro/org/openjdk/bench/java/util/UUIDRandomBench.java b/test/micro/org/openjdk/bench/java/util/UUIDRandomBench.java new file mode 100644 index 000000000000..a55050153934 --- /dev/null +++ b/test/micro/org/openjdk/bench/java/util/UUIDRandomBench.java @@ -0,0 +1,49 @@ +/* + * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package org.openjdk.bench.java.util; + +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; + +@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@Fork(3) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@BenchmarkMode(Mode.Throughput) +public class UUIDRandomBench { + + @Benchmark + @Threads(1) + public UUID single() { + return UUID.randomUUID(); + } + + @Benchmark + @Threads(Threads.MAX) + public UUID max() { + return UUID.randomUUID(); + } + +}