Java 7或Java 6生成随机UUID的性能表现

19

我有一个基于web的Java应用程序,用于生成会话信息的随机UUID。我们的一位测试人员声称根据他自己的分析,生成UUID需要高达350毫秒的时间,但我还没有能够复制他的结果。他指出这篇文章http://www.cowtowncoder.com/blog/archives/2010/10/entry_429.html来支持他的结果。我想看看其他人是否在Java 6或Java 7应用程序中使用Java内置的UUID生成功能时遇到了类似的限制。


生成一个UUID需要350毫秒?他是如何用你提供的文章支持这个说法的? - jarnbjo
这是他正在使用的分析工具。我还无法在我的端上复制。这就是为什么我想检查一下是否有其他人遇到过类似的问题。可能是分析工具的问题,或者可能是Java与运行环境的奇怪组合。对我来说,这也很奇怪。 - Shawn H
再问一遍:他声称生成一个UUID需要350毫秒(因为你使用了复数形式的UUID,但没有具体说明有多少个)?他是如何用你提供的那篇文章支持这个说法的?那篇文章中并没有任何暗示UUID生成器如此缓慢的内容。另一个问题:测试者在哪个操作系统上运行测试?Linux上的Java使用/dev/urandom生成器,如果该系统上没有太多活动(例如用户输入或网络流量),则可能会相当缓慢。 - jarnbjo
该声明称生成单个UUID需要350毫秒。我们的默认系统是运行Mountain Lion的Macbook Pro。生产系统是最新版本的CentOS。 - Shawn H
350毫秒?我用C语言写了一个程序,在新款MacBook Pro上每秒可以生成400,000个UUID。但这并不是一个公平的比较 ;) - Jerfov2
@Jerfov2 不是“加密安全的”吗?https://crypto.stackexchange.com/a/3525/74491 - rogerdpack
7个回答

26

这是在 beta 127 中进行的测试。

请记住,这个测试是 不现实的,超出了我能想象到的任何最坏情况。我的目标是让那些没有事实支持批评UUID使用的人安静下来。

场景:

  • 一个调用java.util.UUID.randomUUID()的百万次紧密循环
    • 仅有这个测试(无竞争)
    • 一个有竞争的测试,其中另外两个线程在一个紧密的循环中进行了1000万次调用。
  • Java 8 beta 127
    • java版本“1.8.0”
    • Java(TM) SE Runtime Environment (build 1.8.0-b127)
    • Java HotSpot(TM) 64-Bit Server VM (build 25.0-b69, mixed mode)
  • 从Netbeans 7.4 IDE运行
  • 在虚拟机内执行
  • Mac mini (2012年底)
    • Mavericks
    • Intel i7 四核处理器,支持超线程技术(8个逻辑核心)
    • 16GB内存

无竞争

在一个线程中运行一个循环,因此没有对同步方法/类的争用。

// Warm the random generator.
java.util.UUID uuid;
uuid = java.util.UUID.randomUUID();

long stop = 0;
long start = System.nanoTime();

int loops = 1000000;  // One million.
for ( int i = 0; i < loops; i++ ) {
    uuid = java.util.UUID.randomUUID();
}

stop = System.nanoTime();

long elapsed = ( stop - start );

System.out.println( "UUIDs: " + loops );
System.out.println( "Nanos: " + elapsed );
System.out.println( "Nanos per uuid: " + ( elapsed / loops ) + " ( micros per: " + ( elapsed / loops / 1000 ) + " )" );

结果

每个 UUID 大约需要 2 微秒

有争用情况

与上面类似,但在执行一百万次调用的循环时,我们还有另外两个线程运行,每个线程分别进行了 次百万次的调用。

// Warm the random generator.
java.util.UUID uuid;
uuid = java.util.UUID.randomUUID();

int pass = 10_000_000 ;  // Ten million.
MyThread t1 = new MyThread( pass );
MyThread t2 = new MyThread( pass );


t1.start();
t2.start();
t3.start();

long stop = 0;
long start = System.nanoTime();

int loops = 1_000_000 ;  // One million.
for ( int i = 0; i < loops; i++ ) {
    uuid = java.util.UUID.randomUUID();
}

stop = System.nanoTime();

long elapsed = ( stop - start );

System.out.println( "UUIDs: " + loops );
System.out.println( "Nanos: " + elapsed );
System.out.println( "Nanos per uuid: " + ( elapsed / loops ) + " ( micros per: " + ( elapsed / loops / 1000 ) + " )" );

而定义每个线程的类...

class MyThread extends Thread {

    private int loops;

    public MyThread( int loops ) {
        this.loops = loops;
    }

    @Override
    public void run() {
        java.util.UUID uuid;
        for ( int i = 0; i < this.loops; i++ ) {
            uuid = java.util.UUID.randomUUID();
        }

    }
}

结果

每个UUID大约需要20微秒

运行时间为14、20、20、23和24微秒每个UUID(不按顺序)。因此,在极端争用情况下,只差了约10倍,20微秒在我所知的任何实际应用中都是可以接受的。


5
嗯,如果我没记错的话,你的数学是 nanos / 1000,这是微秒,而不是毫秒。请纠正我如果我错了。 - Groostav
3
你是否在任何地方使用了变量uuid?如果你没有使用它,那么当JVM检测到一个变量未被使用时,它会优化代码,这可能导致你测试的内容为空。建议查阅Scott Oaks所著《Java性能优化》。你可能需要声明一个volatile字段来确保基准测试正在测量有用的内容。 - Jorge Viana
没有任何f..in的方式可以在20微秒内生成真正的随机UUID,更不用说在有竞争的环境中了。 - vach

14

我测试过了

    for (;;) {
        long t0 = System.currentTimeMillis();
        for (int i = 0; i < 1000000; i++) {
            UUID.randomUUID();
        }
        System.out.println(System.currentTimeMillis() - t0);
    }

在我的电脑上,它的时间大约为1100毫秒,速度相当慢。UUID.randomUUID() 在内部使用 SecureRandom,为了让它更快,我们可以使用常规的 java.util.Random。

    Random r = new Random();
    for (;;) {
            ..
            new UUID(r.nextLong(), r.nextLong());

大约需要80毫秒


4
这些数字不太可信。生成一百万个随机UUID只需要1110毫秒听起来非常快,但是生成一个UUID需要1110毫秒听起来太慢了。 - Stephen C
14
使用SecureRandom是有原因的,不应该“使用普通的java.util.Random”,也不要期望UUID冲突。 - Mikkel Løkke
3
我不确定你的推理是否正确。我认为最大的问题是可预测性,而不是一致性。UUID可能用于安全目的 - 作为不可猜测的密钥 - 这需要高质量的熵。但在许多情况下,如果目标是避免碰撞而不考虑安全性,我相信random.nextLong()策略就足够了。 - Groostav
1
@MikkelLøkke 你认为uuid可以被“轻松地暴力破解”吗?考虑每秒1亿次,连续30年大约是2^60,这比uuid空间的平方根要小得多,但比整个星球在那段时间内可能生成的数量要多得多,当然也包括单个应用程序。您能详细说明一下您认为类型4 uuid如何被轻松地暴力破解吗? - corsiKa
1
@corsiKa 是的,但这个答案是特别关于通过使用java.util.Random来提高性能,而在我看来这是一个可怕的想法,除非你完全理解后果,这就是我说当他们可以被轻易地暴力破解时我所阐述的。 - Mikkel Løkke
显示剩余5条评论

14

UUID的随机形式通常使用“加密强度”随机数源。

如果不这样做,那么所谓的随机UUID就会变得可预测,并且给定UUID被重新发行的概率可能会增加到令人担忧的水平。正如另一个答案建议的那样,您可以为UUID构造函数提供快速(但弱)的伪随机数生成器。但这是一个坏主意。

典型的加密强度随机数生成器使用外部应用程序的熵源。它可能是硬件随机数生成器,但更常见的是在正常运行中由操作系统收集的“随机性”。问题是熵源有速率限制。如果你在一段时间内超过了那个速率,你就会耗尽资源。接下来会发生什么取决于系统,但在某些系统上,读取熵的系统调用将停顿……直到更多的熵可用。

我认为这就是客户端系统上正在发生的事情。(在虚拟机上很常见…)

Linux系统的一个hacky解决方案是安装rngd守护进程并配置它使用良好的伪随机数生成器“填充”熵池。一个安全专家会指出:

  • 这将影响您的UUID生成器的随机性,并且
  • 熵池用于其他安全相关事项,因此从可疑来源进行补充会削弱系统上许多事物的安全性。

我不确定这个hack在实践中有多安全。

以下是关于慢随机数生成的另一个问题的问答:


2
我认为我们也遇到了“熵不足”的问题。UUID生成开始很快,但几个小时后可能会变得缓慢。在无人机上运行可能会加剧问题,因为操作系统的熵来源较少。有关更多信息,请参见https://dev59.com/73VC5IYBdhLWcg3w9GA9。 - Dr. Mike Hopper
用户Basil Bourque的答案包括一个基准测试,每个UUID平均需要20微秒,在3个线程竞争的基准测试中,每个线程尽可能快地生成UUID。您认为熵速率限制是否会影响这样一个基准测试,如果它运行了很长一段时间,其中每个UUID的平均微秒数将在更长时间的测试运行中增加? - Matt Welke
我不知道。试着自己运行一下,看看它是否能正常工作。 - Stephen C

9

线程数量对UUID生成的性能有很大影响。看一下SecureRandom#nextBytes(byte[]方法的实现,它为UUID.randomUUID()生成随机数:

synchronized public void nextBytes(byte[] bytes) {
    secureRandomSpi.engineNextBytes(bytes);
}

nextBytes方法是synchronized的,当被不同线程访问时会导致显著的性能损失。


6

使用版本1而非版本4

考虑使用版本1的UUID类型如何?

版本1 基于MAC地址和当前时间("时空")。比版本4更不容易发生冲突。

版本4 完全基于使用加密强随机数生成器生成的随机数字。

Oracle JVM没有提供版本1生成器,显然是出于安全和隐私方面的考虑。JVM不提供访问主机机器的MAC地址。

JUG库

至少有一个第三方库可用,它提供了版本1 UUID以及其他版本:JUG - Java UUID Generator。他们说Java 6中引入的功能使他们可以访问MAC地址。

测试结果: 20倍

阅读2010年文章中使用Java UUID生成器版本3进行性能讨论的测试结果,更多关于Java UUID生成器(JUG)的内容,有关性能的说明。Tatu Saloranta在他的MacBook上测试了各种类型的UUID。

结论: MAC+时间版本比随机版本快20倍。

基于时间的变体(以太网地址加时间戳)要快得多 - 几乎比默认的基于随机的变体快20倍 - 每秒生成约500万个UUID。


1
在JDK 1.7.0_40下运行的JUnit测试:
package org.corba.util;

import org.junit.Test;
import org.springframework.util.StopWatch;

import java.util.UUID;

/**
 * Test of performance of Java's UUID generation
 * @author Corba Da Geek
 * Date: 1/6/14
 * Time: 3:48 PM
 */
public class TestRandomUUID {
    private static final int ITERATIONS = 1000000;

    @Test
    public void testRandomUUID() throws Exception {
        // Set up data
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();

        // Run test
        for (int i = 0; i < ITERATIONS; i++)
            UUID.randomUUID();

        // Check results
        stopWatch.stop();
        final long totalTimeMillis = stopWatch.getTotalTimeMillis();
        System.out.println("Number of milliseconds: " + totalTimeMillis + " for " + ITERATIONS + " iterations.");
        System.out.println(String.format("Average time per iteration: %.7f ms", (float)totalTimeMillis/ITERATIONS));
    }
}

我的i5笔记本电脑的结果是:

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running org.corba.util.TestRandomUUID
Number of milliseconds: 677 for 1000000 iterations.
Average time per iteration: 0.0006770 ms
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.746 sec

Results :

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

每次调用0.0006770毫秒


4
你确定JVM没有删除UUID.randomUUID();这一行代码吗?因为它的结果没有被使用。 - Dariusz Mydlarz

0

我做了和其他人一样的测试,我的结果是每个UUID生成大约需要300纳秒。测试结果基于一台i7四核WIN7 64位电脑,并且我尝试使用了jdk1.7.0_67和jdk1.8.0_40这两个64位JVM。

我有点困惑为什么我的结果和其他人那么不同...但是每生成一个随机数就需要1毫秒看起来确实有点多!

public static void main(String[] args) throws Exception {
    long start = System.nanoTime();
    int loops = 1000000; // One million.

    long foo = 0;

    for (int i = 0; i < loops; i++) {
        UUID uuid = java.util.UUID.randomUUID();

        //this is just to make sure there isn't some kind of optimization
        //that would prevent the actual generation
        foo += (uuid.getLeastSignificantBits()
                + uuid.getMostSignificantBits());
    }

    long stop = System.nanoTime();

    long elapsed = (stop - start);

    System.out.println(String.format("UUIDs              : %,d", loops));
    System.out.println(String.format("Total time (ns)    : %,d", elapsed));
    System.out.println(String.format("Time per UUID (ns) : %,d", (elapsed / loops)));

    System.out.println();
    System.out.println(foo);
}

输出:

UUIDs              : 1 000 000
Total time (ns)    : 320 715 288
Time per UUID (ns) : 320

5372630452959404665

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接