冷飯新炒:理解JDK中UUID的底層實現
阿新 • • 發佈:2021-01-29
## 前提
`UUID`是`Universally Unique IDentifier`的縮寫,翻譯為通用唯一識別符號或者全域性唯一識別符號。對於`UUID`的描述,下面摘錄一下規範檔案`A Universally Unique IDentifier (UUID) URN Namespace`中的一些描述:
> UUID(也稱為GUID)定義了統一資源名稱名稱空間。UUID的長度為128位元,可以保證在空間和時間上的唯一性。
**動機:**
> 使用UUID的主要原因之一是不需要集中式管理,其中一種格式限定了IEEE 802節點識別符號,其他格式無此限制。可以自動化按需生成UUID,應用於多重不同的場景。UUID演算法支援極高的分配速率,每臺機器每秒鐘可以生成超過1000萬個UUID,因此它們可以作為事務ID使用。UUID具有固定大小128位元,與其他替代方案相比,它具有體積小的優勢,非常適用於各種排序、雜湊和儲存在資料庫中,具有程式設計易用性的特點。
這裡只需要記住`UUID`幾個核心特定:
- 全域性時空唯一性
- 固定長度`128`位元,也就是`16`位元組(`1 byte = 8 bit`)
- 分配速率極高,單機每秒可以生成超過`1000`萬個`UUID`(實際上更高)
下面就`JDK`中的`UUID`實現詳細分析一下`UUID`生成演算法。編寫本文的時候選用的`JDK`為`JDK11`。
## 再聊UUID
前面為了編寫簡單的摘要,所以只粗略摘錄了規範檔案裡面的一些章節,這裡再詳細聊聊`UUID`的一些定義、碰撞概率等等。
### UUID定義
`UUID`是一種軟體構建的標準,也是開放軟體基金會組織在分散式計算環境領域的一部分。提出此標準的目的是:讓分散式系統中的所有元素或者元件都有唯一的可辨別的資訊,因為極低衝突頻率和高效演算法的基礎,它不需要集中式控制和管理唯一可辨別資訊的生成,由此,每個使用者都可以自由地建立與其他人不衝突的`UUID`。
**`UUID`本質是一個`128`位元的數字**,這是一個位長巨大的數值,理論上來說,`UUID`的總數量為`2^128`個。這個數字大概可以這樣估算:如果**每納秒**產生**1兆**個不相同的`UUID`,需要花費超過`100`億年才會用完所有的`UUID`。
### UUID的變體與版本
`UUID`標準和演算法定義的時候,為了考慮歷史相容性和未來的擴充套件,提供了多種變體和版本。接下來的變體和版本描述來源於維基百科中的`Versions`章節和`RFC 4122`中的`Variant`章節。
目前已知的變體如下:
- 變體`0xx`:`Reserved, NCS backward compatibility`,為向後相容做預留的變體
- 變體`10x`:`The IETF aka Leach-Salz variant (used by this class)`,稱為`Leach–Salz UUID`或者`IETF UUID`,`JDK`中`UUID`目前正在使用的變體
- 變體`110`:`Reserved, Microsoft Corporation backward compatibility`,微軟早期`GUID`預留變體
- 變體`111`:`Reserved 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`位置換為`POSIX`的`UID`或`GID`
- `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`是由`16`個`8`位元數字,或者說`32`個`16`進製表示形式下的字元組成,一般表示形式為`8-4-4-4-12`,加上連線字元`-`一共有`36`個字元,例如:
```shell
## 例子
123e4567-e89b-12d3-a456-426614174000
## 通用格式
xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx
```
其中`4`位元長度的`M`和`1`到`3`位元長度的`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|最高位`1`到`3`位元表示變體編號,剩下的`13`到`15`位元表示時鐘序列|
|`node`|節點ID|6|12|`48`位元表示的節點ID|
基於這個表格畫一個圖:
![](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202101/j-u-u-i-d-4.png)
**嚴重注意,重複三次**:
- 上面提到的`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`的碰撞機率呢?這是一個數學問題,可以使用比較著名的**生日悖論**解決:
![](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202101/j-u-u-i-d-1.png)
上圖來源於某搜尋引擎百科。剛好維基百科上給出了碰撞機率的計算過程,其實用的也是生日悖論的計算方法,這裡貼一下:
![](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202101/j-u-u-i-d-2.png)
上面的碰撞機率計算是基於`Leach–Salz`變體和版本`4`進行,得到的結論是:
- `103`萬億個`UUID`中找到重複項的概率是十億分之一
- 要生成一個衝突率達到`50%`的`UUID`至少需要生成`2.71 * 1_000_000^3`個`UUID`
有生之年不需要擔心`UUID`衝突,出現的可能性比大型隕石撞地球還低。
![](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202101/j-u-u-i-d-3.jpg)
### 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`形式表示,例如:
```java
String uuid = UUID.randomUUID().toString();
```
其他`Getter`方法:
```java
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();
```
可以驗證一下不同靜態工廠方法的版本和變體號:
```java
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.UUID`被`final`修飾,實現了`Serializable`和`Comparable`介面,從一般理解上看,有下面的特定:
- 不可變,一般來說工具類都是這樣定義的
- 可序列化和反序列化
- 不同的物件之間可以進行比較,比較方法後面會分析
下面會從不同的方面分析一下`java.util.UUID`的原始碼實現:
- 屬性和建構函式
- 隨機數版本實現
- namespace name-based MD5版本實現
- 其他實現
- 格式化輸出
- 比較相關的方法
### 屬性和建構函式
前面反覆提到`JDK`中只提供了版本`3`和版本`4`的實現,但是`java.util.UUID`的佈局採用了`UUID`規範中的欄位定義,長度一共`128`位元,剛好可以存放在兩個`long`型別的整數中,所以看到了`UUID`類中存在兩個`long`型別的整型數值:
```java
public final class UUID implements java.io.Serializable, Comparable {
// 暫時省略其他程式碼
/*
* 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|`variant`和`clock_seq`加起來等於4|
|`node `|48|12|
接著看`UUID`的其他成員屬性和建構函式:
```java
public final class UUID implements java.io.Serializable, Comparable {
// 暫時省略其他程式碼
// 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)`中有一些位運算技巧:
```shell
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;
```
輸入的位元組陣列長度為`16`,`mostSigBits`由位元組陣列的前`8`個位元組轉換而來,而`leastSigBits`由位元組陣列的後`8`個位元組轉換而來。中間變數`msb`或者`lsb`在提取位元組位進行計算的時候:
- 先進行左移`8`位確保需要計算的位為`0`,已經計算好的位移動到左邊
- 然後右邊需要提取的位元組`data[i]`的`8`位會先和`0xff`(補碼`1111 1111`)進行或運算,確保不足`8`位的高位被補充為`0`,超過`8`位的高位會被截斷為低`8`位,也就是`data[i] & 0xff`確保得到的補碼為`8`位
- 前面兩步的結果再進行或運算
一個模擬過程如下:
```shell
(為了區分明顯,筆者每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`的位元組陣列的所有位就會轉移到`mostSigBits`和`leastSigBits`中。
### 隨機數版本實現
建構函式分析完,接著分析重磅的靜態工廠方法`UUID#randomUUID()`,這是使用頻率最高的一個方法:
```java
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 */
// 清空變體號所在的位,重新設定為2
randomBytes[8] &= 0x3f; /* clear variant */
randomBytes[8] |= 0x80; /* set to IETF variant */
return new UUID(randomBytes);
}
```
關於上面的位運算,這裡可以使用極端的例子進行推演:
```shell
假設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`實現會選用不同的引擎,常見的如`NativePRNG`。`JDK11`配置檔案`$JAVA_HOME/conf/security/java.security`中的`securerandom.source`屬性用於指定系統預設的隨機源:
![](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202101/j-u-u-i-d-5.png)
這裡要提一個小知識點,想要得到密碼學意義上的安全隨機數,可以直接使用真隨機數產生器產生的隨機數,或者使用真隨機數產生器產生的隨機數做種子。通過查詢一些資料得知**非物理真隨機數產生器**有:
- `Linux`作業系統的`/dev/random`裝置介面
- `Windows`作業系統的`CryptGenRandom`介面
如果不修改`java.security`配置檔案,預設隨機數提供引擎會根據不同的作業系統選用不同的實現,這裡不進行深究。在`Linux`環境下,`SecureRandom`例項化後,不通過`setSeed()`方法設定隨機數作為種子,預設就是使用`/dev/random`提供的安全隨機數介面獲取種子,產生的隨機數是密碼學意義上的安全隨機數。**一句話概括,`UUID`中的私有靜態內部類`Holder`中的`SecureRandom`例項可以產生安全隨機數,這個是`JDK`實現`UUID`版本`4`的一個重要前提**。這裡總結一下隨機數版本`UUID`的實現步驟:
- 通過`SecureRandom`依賴提供的安全隨機數介面獲取種子,生成一個`16`位元組的隨機數(位元組陣列)
- 對於生成的隨機數,清空和重新設定`version`和`variant`對應的位
- 把重置完`version`和`variant`的隨機數的所有位轉移到`mostSigBits`和`leastSigBits`中
### namespace name-based MD5版本實現
接著分析版本`3`也就是`namespace name-based MD5`版本的實現,對應於靜態工廠方法`UUID#nameUUIDFromBytes()`:
```java
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`位元組長度的。使用方式很簡單:
```java
UUID uuid = UUID.nameUUIDFromBytes("throwable".getBytes());
```
`namespace name-based MD5`版本`UUID`的實現步驟如下:
- 通過輸入的命名位元組陣列基於`MD5`演算法生成一個`16`位元組長度的隨機數
- 對於生成的隨機數,清空和重新設定`version`和`variant`對應的位
- 把重置完`version`和`variant`的隨機數的所有位轉移到`mostSigBits`和`leastSigBits`中
`namespace name-based MD5`版本的`UUID`強依賴於`MD5`演算法,有個明顯的特徵是如果輸入的`byte[] name`一致的情況下,會產生完全相同的`UUID`例項。
### 其他實現
其他實現主要包括:
```java
// 完全定製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()`方法,這個方法會把`mostSigBits`和`leastSigBits`格式化為`8-4-4-4-12`的形式,這裡詳細分析一下格式化的過程。首先從註釋上看格式是:
```shell
----
time_low = 4 * => 4個16進位制8位字元
time_mid = 2 * => 2個16進位制8位字元
time_high_and_version = 4 * => 2個16進位制8位字元
variant_and_sequence = 4 * => 2個16進位制8位字元
node = 4 * => 6個16進位制8位字元
hexOctet = (2個hexDigit)
hexDigit = 0-9a-F(其實就是16進位制的字元)
```
和前文佈局分析時候的提到的內容一致。`UUID#toString()`方法原始碼如下:
```java
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);
}
```
### 比較相關的方法
比較相關方法如下:
```java
// 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))));
}
```
所有比較方法僅僅和`mostSigBits`和`leastSigBits`有關,畢竟這兩個長整型就儲存了`UUID`例項的所有資訊。
## 小結
縱觀`UUID`的原始碼實現,會發現了除了一些精巧的位運算,它的實現是依賴於一些已經完備的功能,包括`MD5`摘要演算法和`SecureRandom`依賴系統隨機源產生安全隨機數。`UUID`之所以能夠成為一種標準,是因為它凝聚了計算機領域前輩鑽研多年的成果,所以現在使用者才能像寫`Hello World`那樣簡單呼叫`UUID.randomUUID()`。
參考資料:
- [RFC 4122](https://www.ietf.org/rfc/rfc4122.txt)
- [維基百科 - Universally unique identifier](https://en.wikipedia.org/wiki/Universally_unique_identifier)
- JDK11相關原始碼
留給讀者的開放性問題:
- `UUID`是利用什麼特性把衝突率降到極低?
- 人類有可能繁衍到`UUID`全部用完的年代嗎?
(本文完 c-2-w e-a-202