我正在编写一个类,当被调用时将调用一个方法来使用系统时间来生成一个唯一的 8 字符字母数字作为参考 ID。但我担心在某些时候,可能会在同一毫秒内进行多个调用,从而导致相同的参考 ID。如何保护对系统时间的调用免受可能同时调用此方法的多个线程的影响?
3 回答
系统时间是唯一 ID 的不可靠来源。而已。不要使用它。您需要某种形式的永久源(UUID 使用安全随机,种子由操作系统提供)
系统时间甚至可能会向后跳几毫秒,并完全搞砸你的逻辑。如果您只能容忍 64 位,您可以使用High/Low 生成器,这是一个非常好的折衷方案,或者自己制作食谱:例如自 2012 年初以来的 18 位天(您还有 700 多年的时间),然后 46 位的随机性来自SecureRandom - 不是最好的情况,从技术上讲它可能会失败,但它不需要外部持久性。
我建议将threadID添加到参考 ID。这将使参考更加独特。然而,即使在一个线程内对时间源的连续调用也可能传递相同的值。即使调用最高分辨率源(QueryPerformanceCounter)也可能在某些硬件上产生相同的值。此问题的一个可能解决方案是针对其前身测试收集的时间值,并将增量项添加到“时间戳”。当这应该是人类可读的时,您可能需要超过 8 个字符。时间戳的最有效来源是 GetSystemTimeAsFileTime API。我在这个答案中写了一些细节。
您可以使用UUID
该类为您的 ID 生成位,然后使用一些位运算符并将Long.toString
其转换为 base-36(字母数字)。
public static String getId() {
UUID uuid = UUID.randomUUID();
// This is the time-based long, and is predictable
long msb = uuid.getMostSignificantBits();
// This contains the variant bits, and is random
long lsb = uuid.getLeastSignificantBits();
long result = msb ^ lsb; // XOR
String encoded = Long.toString(result, 36);
// Remove sign if negative
if (result < 0)
encoded = encoded.substring(1, encoded.length());
// Trim extra digits or pad with zeroes
if (encoded.length() > 8) {
encoded = encoded.substring(encoded.length() - 8, encoded.length());
}
while (encoded.length() < 8) {
encoded = "0" + encoded;
}
}
由于与 相比,您的字符空间仍然更小UUID
,因此这并非万无一失。使用以下代码对其进行测试:
public static void main(String[] args) {
Set<String> ids = new HashSet<String>();
int count = 0;
for (int i = 0; i < 100000; i++) {
if (!ids.add(getId())) {
count++;
}
}
System.out.println(count + " duplicate(s)");
}
对于 100,000 个 ID,代码的性能非常一致并且非常快。当我将另一个数量级增加到 1,000,000 时,我开始获得重复的 ID。我修改了修剪以采用编码字符串的结尾而不是开头,这大大提高了重复 ID 率。现在拥有 1,000,000 个 ID 对我来说不会产生任何重复。
您最好的选择可能仍然是使用同步计数器,AtomicInteger
或AtomicLong
使用上面的代码以 base-36 编码数字,特别是如果您计划拥有大量 ID。
编辑:计数器方法,以防万一:
private final AtomicLong counter;
public IdGenerator(int start) {
// start could also be initialized from a file or other
// external source that stores the most recently used ID
counter = new AtomicLong(start);
}
public String getId() {
long result = counter.getAndIncrement();
String encoded = Long.toString(result, 36);
// Remove sign if negative
if (result < 0)
encoded = encoded.substring(1, encoded.length());
// Trim extra digits or pad with zeroes
if (encoded.length() > 8) {
encoded = encoded.substring(0, 8);
}
while (encoded.length() < 8) {
encoded = "0" + encoded;
}
}
此代码是线程安全的,可以并发访问。