冷飯新炒:理解JDK中UUID的底層實現

前提

UUIDUniversally Unique IDentifier的縮寫,翻譯爲通用惟一標識符或者全局惟一標識符。對於UUID的描述,下面摘錄一下規範文件A Universally Unique IDentifier (UUID) URN Namespace中的一些描述:java

UUID(也稱爲GUID)定義了統一資源名稱命名空間。UUID的長度爲128比特,能夠保證在空間和時間上的惟一性。node

動機:git

使用UUID的主要緣由之一是不須要集中式管理,其中一種格式限定了IEEE 802節點標識符,其餘格式無此限制。能夠自動化按需生成UUID,應用於多重不一樣的場景。UUID算法支持極高的分配速率,每臺機器每秒鐘能夠生成超過1000萬個UUID,所以它們能夠做爲事務ID使用。UUID具備固定大小128比特,與其餘替代方案相比,它具備體積小的優點,很是適用於各類排序、散列和存儲在數據庫中,具備編程易用性的特色。算法

這裏只須要記住UUID幾個核心特定:shell

  • 全局時空惟一性
  • 固定長度128比特,也就是16字節(1 byte = 8 bit)
  • 分配速率極高,單機每秒能夠生成超過1000萬個UUID(實際上更高)

下面就JDK中的UUID實現詳細分析一下UUID生成算法。編寫本文的時候選用的JDKJDK11數據庫

再聊UUID

前面爲了編寫簡單的摘要,因此只粗略摘錄了規範文件裏面的一些章節,這裏再詳細聊聊UUID的一些定義、碰撞機率等等。編程

UUID定義

UUID是一種軟件構建的標準,也是開放軟件基金會組織在分佈式計算環境領域的一部分。提出此標準的目的是:讓分佈式系統中的全部元素或者組件都有惟一的可辨別的信息,由於極低衝突頻率和高效算法的基礎,它不須要集中式控制和管理惟一可辨別信息的生成,由此,每一個使用者均可以自由地建立與其餘人不衝突的UUID數組

UUID本質是一個128比特的數字,這是一個位長巨大的數值,理論上來講,UUID的總數量爲2^128個。這個數字大概能夠這樣估算:若是每納秒產生1兆個不相同的UUID,須要花費超過100億年纔會用完全部的UUID安全

UUID的變體與版本

UUID標準和算法定義的時候,爲了考慮歷史兼容性和將來的擴展,提供了多種變體和版本。接下來的變體和版本描述來源於維基百科中的Versions章節和RFC 4122中的Variant章節。框架

目前已知的變體以下:

  • 變體0xxReserved, NCS backward compatibility,爲向後兼容作預留的變體
  • 變體10xThe IETF aka Leach-Salz variant (used by this class),稱爲Leach–Salz UUID或者IETF UUIDJDKUUID目前正在使用的變體
  • 變體110Reserved, Microsoft Corporation backward compatibility,微軟早期GUID預留變體
  • 變體111Reserved for future definition,未來擴展預留,目前還沒被使用的變體

目前已知的版本以下:

  • UUID(特殊版本0),用00000000-0000-0000-0000-000000000000表示,也就是全部的比特都是0
  • date-time and MAC address(版本1):基於時間和MAC地址的版本,經過計算當前時間戳、隨機數和機器MAC地址獲得。因爲有MAC地址,這個能夠保證其在全球的惟一性。可是使用了MAC地址,就會有MAC地址暴露問題。如果局域網,能夠用IP地址代替
  • date-time and MAC address, DCE security version(版本2):分佈式計算環境安全的UUID,算法和版本1基本一致,但會把時間戳的前4位置換爲POSIXUIDGID
  • namespace name-based MD5(版本3):經過計算名字和命名空間的MD5散列值獲得。這個版本的UUID保證了:相同命名空間中不一樣名字生成的UUID的惟一性;不一樣命名空間中的UUID的惟一性;相同命名空間中相同名字的UUID重複生成是相同的
  • random(版本4):根據隨機數,或者僞隨機數生成UUID。這種UUID產生重複的機率是能夠計算出來的,還有一個特色就是預留了6比特存放變體和版本屬性,因此隨機生成的位一共有122個,總量爲2^122,比其餘變體的總量要偏少
  • namespace name-based SHA-1(版本5):和版本3相似,散列算法換成了SHA-1

其中,JDK中應用的變體是Leach-Salz,提供了namespace name-based MD5(版本3)和random(版本4)兩個版本的UUID生成實現。

UUID的格式

在規範文件描述中,UUID是由168比特數字,或者說3216進製表示形式下的字符組成,通常表示形式爲8-4-4-4-12,加上鍊接字符-一共有36個字符,例如:

## 例子
123e4567-e89b-12d3-a456-426614174000
## 通用格式
xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx

其中4比特長度的M13比特長度的N分別表明版本號和變體標識。UUID的具體佈局以下:

屬性 屬性名 長度(bytes 長度(16進制字符) 內容
time_low 時間戳低位 4 8 表明時間戳的低32比特的整數表示
time_mid 時間戳中位 2 4 表明時間戳的中間16比特的整數表示
time_hi_and_version 時間戳高位和版本號 2 4 高位4比特是版本號表示,剩餘是時間戳的高12比特的整數表示
clock_seq_hi_and_res clock_seq_low 時鐘序列與變體編號 2 4 最高位13比特表示變體編號,剩下的1315比特表示時鐘序列
node 節點ID 6 12 48比特表示的節點ID

基於這個表格畫一個圖:

嚴重注意,重複三次

  • 上面提到的UUID的具體佈局只適用於date-time and MAC address(版本1)和date-time and MAC address, DCE security version(版本2),其餘版本雖然採用了基本同樣的字段分佈,可是沒法獲取時間戳、時鐘序列或者節點ID等信息
  • 上面提到的UUID的具體佈局只適用於date-time and MAC address(版本1)和date-time and MAC address, DCE security version(版本2),其餘版本雖然採用了基本同樣的字段分佈,可是沒法獲取時間戳、時鐘序列或者節點ID等信息
  • 上面提到的UUID的具體佈局只適用於date-time and MAC address(版本1)和date-time and MAC address, DCE security version(版本2),其餘版本雖然採用了基本同樣的字段分佈,可是沒法獲取時間戳、時鐘序列或者節點ID等信息

JDK中只提供了版本3和版本4的實現,可是java.util.UUID的佈局採用了上面表格的字段

UUID的碰撞概率計算

UUID的總量雖然巨大,可是若是不停地使用,假設每納秒生成超過1兆個UUID而且人類有幸可以繁衍到100億年之後,總會有可能產生重複的UUID。那麼,怎麼計算UUID的碰撞概率呢?這是一個數學問題,可使用比較著名的生日悖論解決:

上圖來源於某搜索引擎百科。恰好維基百科上給出了碰撞概率的計算過程,其實用的也是生日悖論的計算方法,這裏貼一下:

上面的碰撞概率計算是基於Leach–Salz變體和版本4進行,獲得的結論是:

  • 103萬億個UUID中找到重複項的機率是十億分之一
  • 要生成一個衝突率達到50%UUID至少須要生成2.71 * 1_000_000^3UUID

有生之年不須要擔憂UUID衝突,出現的可能性比大型隕石撞地球還低。

UUID的使用場景

基本全部須要使用全局惟一標識符的場景均可以使用UUID,除非對長度有明確的限制,經常使用的場景包括:

  • 日誌框架映射診斷上下文中的TRACE_ID
  • APM工具或者說OpenTracing規範中的SPAN_ID
  • 特殊場景下數據庫主鍵或者虛擬外鍵
  • 交易ID(訂單ID
  • 等等......

JDK中UUID詳細介紹和使用

這裏先介紹使用方式。前面提到JDK中應用的變體是Leach-Salz(變體2),提供了namespace name-based MD5(版本3)和random(版本4)兩個版本的UUID生成實現,實際上java.util.UUID提供了四種生成UUID實例的方式:

  • 最多見的就是調用靜態方法UUID#randomUUID(),這就是版本4的靜態工廠方法
  • 其次是調用靜態方法UUID#nameUUIDFromBytes(byte[] name),這就是版本3的靜態工廠方法
  • 另外有調用靜態方法UUID#fromString(String name),這是解析8-4-4-4-12格式字符串生成UUID實例的靜態工廠方法
  • 還有低層次的構造函數UUID(long mostSigBits, long leastSigBits),這個對於使用者來講並不常見

最經常使用的方法有實例方法toString(),把UUID轉化爲16進制字符串拼接而成的8-4-4-4-12形式表示,例如:

String uuid = UUID.randomUUID().toString();

其餘Getter方法:

UUID uuid = UUID.randomUUID();
// 返回版本號
int version = uuid.version();
// 返回變體號
int variant = uuid.variant();
// 返回時間戳 - 這個方法會報錯,只有Time-based UUID也就是版本1或者2的UUID實現才能返回時間戳
long timestamp = uuid.timestamp();
// 返回時鐘序列 - 這個方法會報錯,只有Time-based UUID也就是版本1或者2的UUID實現才能返回時鐘序列
long clockSequence = uuid.clockSequence();
// 返回節點ID - 這個方法會報錯,只有Time-based UUID也就是版本1或者2的UUID實現才能返回節點ID
long nodeId = uuid.node();

能夠驗證一下不一樣靜態工廠方法的版本和變體號:

UUID uuid = UUID.randomUUID();
int version = uuid.version();
int variant = uuid.variant();
System.out.println(String.format("version:%d,variant:%d", version, variant));
uuid = UUID.nameUUIDFromBytes(new byte[0]);
version = uuid.version();
variant = uuid.variant();
System.out.println(String.format("version:%d,variant:%d", version, variant));
// 輸出結果
version:4,variant:2
version:3,variant:2

探究JDK中UUID源碼實現

java.util.UUIDfinal修飾,實現了SerializableComparable接口,從通常理解上看,有下面的特定:

  • 不可變,通常來講工具類都是這樣定義的
  • 可序列化和反序列化
  • 不一樣的對象之間能夠進行比較,比較方法後面會分析

下面會從不一樣的方面分析一下java.util.UUID的源碼實現:

  • 屬性和構造函數
  • 隨機數版本實現
  • namespace name-based MD5版本實現
  • 其餘實現
  • 格式化輸出
  • 比較相關的方法

屬性和構造函數

前面反覆提到JDK中只提供了版本3和版本4的實現,可是java.util.UUID的佈局採用了UUID規範中的字段定義,長度一共128比特,恰好能夠存放在兩個long類型的整數中,因此看到了UUID類中存在兩個long類型的整型數值:

public final class UUID implements java.io.Serializable, Comparable<UUID> {
   
    // 暫時省略其餘代碼

    /*
     * The most significant 64 bits of this UUID.
     * UUID中有效的高64比特
     *
     * @serial
     */
    private final long mostSigBits;

    /*
     * The least significant 64 bits of this UUID.
     *  UUID中有效的低64比特
     *
     * @serial
     */
    private final long leastSigBits;
    
    // 暫時省略其餘代碼
}

UUID類註釋中能夠看到具體的字段佈局以下:

64比特mostSigBits的佈局

字段 bit長度 16進制字符長度
time_low 32 8
time_mid 16 4
version 4 1
time_hi 12 3

64比特leastSigBits的佈局

字段 bit長度 16進制字符長度
variant 2 小於1
clock_seq 14 variantclock_seq加起來等於4
node 48 12

接着看UUID的其餘成員屬性和構造函數:

public final class UUID implements java.io.Serializable, Comparable<UUID> {
   
    // 暫時省略其餘代碼
    
    // Java語言訪問類,裏面存放了不少底層相關的訪問或者轉換方法,在UUID中主要是toString()實例方法用來格式化成8-4-4-4-12的形式,委託到Long.fastUUID()方法
    private static final JavaLangAccess jla = SharedSecrets.getJavaLangAccess();

    // 靜態內部類確保SecureRandom初始化,用於版本4的隨機數UUID版本生成安全隨機數
    private static class Holder {
        static final SecureRandom numberGenerator = new SecureRandom();
    }
    
    // 經過長度爲16的字節數組,計算mostSigBits和leastSigBits的值初始化UUID實例
    private UUID(byte[] data) {
        long msb = 0;
        long lsb = 0;
        assert data.length == 16 : "data must be 16 bytes in length";
        for (int i=0; i<8; i++)
            msb = (msb << 8) | (data[i] & 0xff);
        for (int i=8; i<16; i++)
            lsb = (lsb << 8) | (data[i] & 0xff);
        this.mostSigBits = msb;
        this.leastSigBits = lsb;
    }
    
    // 直接指定mostSigBits和leastSigBits構造UUID實例
    public UUID(long mostSigBits, long leastSigBits) {
        this.mostSigBits = mostSigBits;
        this.leastSigBits = leastSigBits;
    }

    // 暫時省略其餘代碼
}

私有構造private UUID(byte[] data)中有一些位運算技巧:

long msb = 0;
long lsb = 0;
assert data.length == 16 : "data must be 16 bytes in length";
for (int i=0; i<8; i++)
    msb = (msb << 8) | (data[i] & 0xff);
for (int i=8; i<16; i++)
    lsb = (lsb << 8) | (data[i] & 0xff);
this.mostSigBits = msb;
this.leastSigBits = lsb;

輸入的字節數組長度爲16mostSigBits由字節數組的前8個字節轉換而來,而leastSigBits由字節數組的後8個字節轉換而來。中間變量msb或者lsb在提取字節位進行計算的時候:

  • 先進行左移8位確保須要計算的位爲0,已經計算好的位移動到左邊
  • 而後右邊須要提取的字節data[i]8位會先和0xff(補碼1111 1111)進行或運算,確保不足8位的高位被補充爲0,超過8位的高位會被截斷爲低8位,也就是data[i] & 0xff確保獲得的補碼爲8
  • 前面兩步的結果再進行或運算

一個模擬過程以下:

(爲了區分明顯,筆者每4位加了一個下劃線)

(爲了簡答,只看字節數組的前4個字節,同時只看long類型的前4個字節)

0xff === 1111_1111

long msb = 0  => 0000_0000 0000_0000 0000_0000 0000_0000

byte[] data
0000_0001 0000_0010 0000_0100 0000_1000

i = 0(第一輪)
msb << 8 = 0000_0000 0000_0000 0000_0000 0000_0000
data[i] & 0xff = 0000_0001 & 1111_1111 = 0000_0001
(msb << 8) | (data[i] & 0xff) = 0000_0000 0000_0000 0000_0000 0000_0001

(第一輪 msb = 0000_0000 0000_0000 0000_0000 0000_0001)

i = 1(第二輪)
msb << 8 = 0000_0000 0000_0000 0000_0001 0000_0000
data[i] & 0xff = 0000_0010 & 1111_1111 = 0000_0010
(msb << 8) | (data[i] & 0xff) = 0000_0000 0000_0000 0000_0001 0000_0010

(第二輪 msb = 0000_0000 0000_0000 0000_0001 0000_0010)

i = 2(第三輪)
msb << 8 = 0000_0000 0000_0001 0000_0010 0000_0000
data[i] & 0xff = 0000_0100 & 1111_1111 = 0000_0100
(msb << 8) | (data[i] & 0xff) = 0000_0000 0000_0001 0000_0010 0000_0100

(第三輪 msb = 0000_0000 0000_0001 0000_0010 0000_0100)

i = 3(第四輪)
msb << 8 = 0000_0001 0000_0010 0000_0100 0000000
data[i] & 0xff = 0000_1000 & 1111_1111 = 0000_1000
(msb << 8) | (data[i] & 0xff) = 0000_0001 0000_0010 0000_0100 0000_1000

(第四輪 msb = 0000_0001 0000_0010 0000_0100 0000_1000)

以此類推,這個私有構造函數執行完畢後,長度爲16的字節數組的全部位就會轉移到mostSigBitsleastSigBits中。

隨機數版本實現

構造函數分析完,接着分析重磅的靜態工廠方法UUID#randomUUID(),這是使用頻率最高的一個方法:

public static UUID randomUUID() {
    // 靜態內部類Holder持有的SecureRandom實例,確保提早初始化
    SecureRandom ng = Holder.numberGenerator;
    // 生成一個16字節的安全隨機數,放在長度爲16的字節數組中
    byte[] randomBytes = new byte[16];
    ng.nextBytes(randomBytes);
    // 清空版本號所在的位,從新設置爲4
    randomBytes[6]  &= 0x0f;  /* clear version        */
    randomBytes[6]  |= 0x40;  /* set to version 4     */
    // 清空變體號所在的位,從新設置爲
    randomBytes[8]  &= 0x3f;  /* clear variant        */
    randomBytes[8]  |= 0x80;  /* set to IETF variant  */
    return new UUID(randomBytes);
}

關於上面的位運算,這裏可使用極端的例子進行推演:

假設randomBytes[6] = 1111_1111
// 清空version位
randomBytes[6] &= 0x0f => 1111_1111 & 0000_1111 = 0000_1111
獲得randomBytes[6] = 0000_1111 (這裏可見高4比特被清空爲0)
// 設置version位爲整數4 => 十六進制0x40 => 二級制補碼0100_0000
randomBytes[6] |= 0x40 => 0000_1111 | 0100_0000 = 0100_1111
獲得randomBytes[6] = 0100_1111

結果:version位 => 0100(4 bit)=> 對應十進制數4

同理

假設randomBytes[8] = 1111_1111
// 清空variant位
randomBytes[8] &= 0x3f => 1111_1111 & 0011_1111 = 0011_1111
// 設置variant位爲整數128 => 十六進制0x80 => 二級制補碼1000_0000 (這裏取左邊高位2位)
randomBytes[8] |= 0x80 => 0011_1111 | 1000_0000 = 1011_1111

結果:variant位 => 10(2 bit)=> 對應十進制數2

關於UUID裏面的Getter方法例如version()variant()其實就是找到對應的位,而且轉換爲十進制整數返回,若是熟練使用位運算,應該不難理解,後面不會分析這類的Getter方法。

隨機數版本實現強依賴於SecureRandom生成的隨機數(字節數組)SecureRandom的引擎提供者能夠從sun.security.provider.SunEntries中查看,對於不一樣系統版本的JDK實現會選用不一樣的引擎,常見的如NativePRNGJDK11配置文件$JAVA_HOME/conf/security/java.security中的securerandom.source屬性用於指定系統默認的隨機源:

這裏要提一個小知識點,想要獲得密碼學意義上的安全隨機數,能夠直接使用真隨機數產生器產生的隨機數,或者使用真隨機數產生器產生的隨機數作種子。經過查找一些資料得知非物理真隨機數產生器有:

  • Linux操做系統的/dev/random設備接口
  • Windows操做系統的CryptGenRandom接口

若是不修改java.security配置文件,默認隨機數提供引擎會根據不一樣的操做系統選用不一樣的實現,這裏不進行深究。在Linux環境下,SecureRandom實例化後,不經過setSeed()方法設置隨機數做爲種子,默認就是使用/dev/random提供的安全隨機數接口獲取種子,產生的隨機數是密碼學意義上的安全隨機數。一句話歸納,UUID中的私有靜態內部類Holder中的SecureRandom實例能夠產生安全隨機數,這個是JDK實現UUID版本4的一個重要前提。這裏總結一下隨機數版本UUID的實現步驟:

  • 經過SecureRandom依賴提供的安全隨機數接口獲取種子,生成一個16字節的隨機數(字節數組)
  • 對於生成的隨機數,清空和從新設置versionvariant對應的位
  • 把重置完versionvariant的隨機數的全部位轉移到mostSigBitsleastSigBits

namespace name-based MD5版本實現

接着分析版本3也就是namespace name-based MD5版本的實現,對應於靜態工廠方法UUID#nameUUIDFromBytes()

public static UUID nameUUIDFromBytes(byte[] name) {
    MessageDigest md;
    try {
        md = MessageDigest.getInstance("MD5");
    } catch (NoSuchAlgorithmException nsae) {
        throw new InternalError("MD5 not supported", nsae);
    }
    byte[] md5Bytes = md.digest(name);
    md5Bytes[6]  &= 0x0f;  /* clear version        */
    md5Bytes[6]  |= 0x30;  /* set to version 3     */
    md5Bytes[8]  &= 0x3f;  /* clear variant        */
    md5Bytes[8]  |= 0x80;  /* set to IETF variant  */
    return new UUID(md5Bytes);
}

它的後續基本處理和隨機數版本基本一致(清空版本位的時候,從新設置爲3),惟一明顯不一樣的地方就是生成原始隨機數的時候,採用的方式是:基於輸入的name字節數組,經過MD5摘要算法生成一個MD5摘要字節數組做爲原始安全隨機數,返回的這個隨機數恰好也是16字節長度的。使用方式很簡單:

UUID uuid = UUID.nameUUIDFromBytes("throwable".getBytes());

namespace name-based MD5版本UUID的實現步驟以下:

  • 經過輸入的命名字節數組基於MD5算法生成一個16字節長度的隨機數
  • 對於生成的隨機數,清空和從新設置versionvariant對應的位
  • 把重置完versionvariant的隨機數的全部位轉移到mostSigBitsleastSigBits

namespace name-based MD5版本的UUID強依賴於MD5算法,有個明顯的特徵是若是輸入的byte[] name一致的狀況下,會產生徹底相同的UUID實例。

其餘實現

其餘實現主要包括:

// 徹底定製mostSigBits和leastSigBits,能夠參考UUID標準字段佈局進行設置,也能夠按照自行制定的標準
public UUID(long mostSigBits, long leastSigBits) {
    this.mostSigBits = mostSigBits;
    this.leastSigBits = leastSigBits;
}

// 基於字符串格式8-4-4-4-12的UUID輸入,從新解析出mostSigBits和leastSigBits,這個靜態工廠方法也不經常使用,裏面的位運算也不進行詳細探究
public static UUID fromString(String name) {
    int len = name.length();
    if (len > 36) {
        throw new IllegalArgumentException("UUID string too large");
    }
    int dash1 = name.indexOf('-', 0);
    int dash2 = name.indexOf('-', dash1 + 1);
    int dash3 = name.indexOf('-', dash2 + 1);
    int dash4 = name.indexOf('-', dash3 + 1);
    int dash5 = name.indexOf('-', dash4 + 1);
    if (dash4 < 0 || dash5 >= 0) {
        throw new IllegalArgumentException("Invalid UUID string: " + name);
    }
    long mostSigBits = Long.parseLong(name, 0, dash1, 16) & 0xffffffffL;
    mostSigBits <<= 16;
    mostSigBits |= Long.parseLong(name, dash1 + 1, dash2, 16) & 0xffffL;
    mostSigBits <<= 16;
    mostSigBits |= Long.parseLong(name, dash2 + 1, dash3, 16) & 0xffffL;
    long leastSigBits = Long.parseLong(name, dash3 + 1, dash4, 16) & 0xffffL;
    leastSigBits <<= 48;
    leastSigBits |= Long.parseLong(name, dash4 + 1, len, 16) & 0xffffffffffffL;
    return new UUID(mostSigBits, leastSigBits);
}

格式化輸出

格式化輸出體如今UUID#toString()方法,這個方法會把mostSigBitsleastSigBits格式化爲8-4-4-4-12的形式,這裏詳細分析一下格式化的過程。首先從註釋上看格式是:

<time_low>-<time_mid>-<time_high_and_version>-<variant_and_sequence>-<node>

time_low = 4 * <hexOctet> => 4個16進制8位字符
time_mid = 2 * <hexOctet> => 2個16進制8位字符
time_high_and_version = 4 * <hexOctet> => 2個16進制8位字符
variant_and_sequence = 4 * <hexOctet> => 2個16進制8位字符
node = 4 * <hexOctet> => 6個16進制8位字符

hexOctet = <hexDigit><hexDigit>(2個hexDigit)
hexDigit = 0-9a-F(其實就是16進制的字符)

和前文佈局分析時候的提到的內容一致。UUID#toString()方法源碼以下:

private static final JavaLangAccess jla = SharedSecrets.getJavaLangAccess();


public String toString() {
    return jla.fastUUID(leastSigBits, mostSigBits);
}

↓↓↓↓↓↓↓↓↓↓↓↓

// java.lang.System
private static void setJavaLangAccess() {

    SharedSecrets.setJavaLangAccess(new JavaLangAccess() {
    
        public String fastUUID(long lsb, long msb) {
            return Long.fastUUID(lsb, msb);
        }
}

↓↓↓↓↓↓↓↓↓↓↓↓
// java.lang.Long
static String fastUUID(long lsb, long msb) {
    // COMPACT_STRINGS在String類中默認爲true,因此會命中if分支
    if (COMPACT_STRINGS) {
        // 初始化36長度的字節數組 
        byte[] buf = new byte[36];
        // lsb的低48位轉換爲16進制格式寫入到buf中 - node => 位置[24,35]
        formatUnsignedLong0(lsb,        4, buf, 24, 12);
        // lsb的高16位轉換爲16進制格式寫入到buf中 - variant_and_sequence  => 位置[19,22]
        formatUnsignedLong0(lsb >>> 48, 4, buf, 19, 4);
        // msb的低16位轉換爲16進制格式寫入到buf中 - time_high_and_version => 位置[14,17]
        formatUnsignedLong0(msb,        4, buf, 14, 4); 
        // msb的中16位轉換爲16進制格式寫入到buf中 - time_mid => 位置[9,12]
        formatUnsignedLong0(msb >>> 16, 4, buf, 9,  4);
        // msb的高32位轉換爲16進制格式寫入到buf中 - time_low => 位置[0,7]
        formatUnsignedLong0(msb >>> 32, 4, buf, 0,  8);
        // 空餘的字節槽位插入'-',恰好佔用了4個字節
        buf[23] = '-';
        buf[18] = '-';
        buf[13] = '-';
        buf[8]  = '-';
        // 基於處理好的字節數組,實例化String,而且編碼指定爲LATIN1
        return new String(buf, LATIN1);
    } else {
        byte[] buf = new byte[72];
        formatUnsignedLong0UTF16(lsb,        4, buf, 24, 12);
        formatUnsignedLong0UTF16(lsb >>> 48, 4, buf, 19, 4);
        formatUnsignedLong0UTF16(msb,        4, buf, 14, 4);
        formatUnsignedLong0UTF16(msb >>> 16, 4, buf, 9,  4);
        formatUnsignedLong0UTF16(msb >>> 32, 4, buf, 0,  8);
        StringUTF16.putChar(buf, 23, '-');
        StringUTF16.putChar(buf, 18, '-');
        StringUTF16.putChar(buf, 13, '-');
        StringUTF16.putChar(buf,  8, '-');
        return new String(buf, UTF16);
    }
}

/**
 * 格式化無符號的長整型,填充到字節緩衝區buf中,若是長度len超過了輸入值的ASCII格式表示,則會使用0進行填充
 * 這個方法就是把輸入長整型值val,對應一段長度的位,填充到字節數組buf中,len控制寫入字符的長度,offset控制寫入buf的起始位置
 * 而shift參數決定基礎格式,4是16進制,1是2進制,3是8位
 */
static void formatUnsignedLong0(long val, int shift, byte[] buf, int offset, int len) {
    int charPos = offset + len;
    int radix = 1 << shift;
    int mask = radix - 1;
    do {
        buf[--charPos] = (byte)Integer.digits[((int) val) & mask];
        val >>>= shift;
    } while (charPos > offset);
}

比較相關的方法

比較相關方法以下:

// hashCode方法基於mostSigBits和leastSigBits作異或得出一箇中間變量hilo,再以32爲因子進行計算
public int hashCode() {
    long hilo = mostSigBits ^ leastSigBits;
    return ((int)(hilo >> 32)) ^ (int) hilo;
}

// equals爲實例對比方法,直接對比兩個UUID的mostSigBits和leastSigBits值,徹底相等的時候返回true
public boolean equals(Object obj) {
    if ((null == obj) || (obj.getClass() != UUID.class))
        return false;
    UUID id = (UUID)obj;
    return (mostSigBits == id.mostSigBits &&
            leastSigBits == id.leastSigBits);
}

// 比較規則是mostSigBits高位大者爲大,高位相等的狀況下,leastSigBits大者爲大
public int compareTo(UUID val) {
    // The ordering is intentionally set up so that the UUIDs
    // can simply be numerically compared as two numbers
    return (this.mostSigBits < val.mostSigBits ? -1 :
            (this.mostSigBits > val.mostSigBits ? 1 :
                (this.leastSigBits < val.leastSigBits ? -1 :
                (this.leastSigBits > val.leastSigBits ? 1 :
                0))));
}

全部比較方法僅僅和mostSigBitsleastSigBits有關,畢竟這兩個長整型就存儲了UUID實例的全部信息。

小結

縱觀UUID的源碼實現,會發現了除了一些精巧的位運算,它的實現是依賴於一些已經完備的功能,包括MD5摘要算法和SecureRandom依賴系統隨機源產生安全隨機數。UUID之因此可以成爲一種標準,是由於它凝聚了計算機領域前輩鑽研多年的成果,因此如今使用者才能像寫Hello World那樣簡單調用UUID.randomUUID()

參考資料:

留給讀者的開放性問題:

  • UUID是利用什麼特性把衝突率降到極低?
  • 人類有可能繁衍到UUID所有用完的年代嗎?

(本文完 c-2-w e-a-20210129)

相關文章
相關標籤/搜索