美女同事用單例模式花式實現雪花算法
雪花算法適用於生成全局唯一的編號,比如數據庫主鍵 id,訂單編號等
至於爲什麼叫雪花算法,是因爲科學家通過研究認爲自然界中不存在兩片完全相同的雪花,所以這種算法用雪花來命名也是強調它生成的編號不會重複吧
雪花算法生成的編號共有 64bit,剛好是 java 中 long 的最大範圍
雪花算法是用 64 位的二進制數字表示
在二進制中,第一位是符號位,表示正數或負數,正數是 0,負數是 1
因爲生成唯一編號不需要負數,所以第一位永遠是 0,相當於沒用
用 41 位表示時間戳,這個時間戳是當前時間和指定時間的毫秒差。
比如指定時間是2021-06-30 11:07:20
,在 1 秒之後調用了雪花算法,那麼這個毫秒差就是 1000
41 位的二進制,可以表示 「241-1」 個正數,這麼多個毫秒差理論上是可以使用 69 年。
(241-1) / (1000∗60∗60∗24∗365)≈69.73
IT 行業日益更新的當下,很少有程序能持續使用 69 年。所以,足夠了
用 10 位表示機器編號,在分佈式環境下最多支持 「210=1024」 個機器
用 12 位表示序列號,在同一個毫秒內,同一個機器上用序列號來控制併發。同一個毫秒內可以允許有 「212=4096」 個併發
總結下來就是,即使你的程序在分佈式環境下有 1024 臺負載,每個負載每毫秒的併發量是 4096,雪花算法生成的唯一編號也不會重複,算法不可謂不強大
以上是基於二進制講的雪花算法,比較晦澀難懂,也不利於接下來我們要討論的內容
所以,我們對雪花算法做一點修改,改成如下方式
用 15 個字符表示時間串,比如2021年06月30日14點52分30秒226毫秒
可以表示爲210630145230226
。這麼表示可讀性更強,而且百年之內不會重複
用兩位數字表示機器編號,最多可以支持 100 個機器
用兩位數字表示序列號,一毫秒內支持 100 個併發
接下來把我們改編後的算法用代碼實現一下 (這裏貼的是圖片,文末會附上源碼)
這就實現了我們改編過後的雪花算法。但是,仔細想一下,代碼還存在併發問題
在兩個線程同時執行這塊代碼時獲取的唯一編號有可能重複
這是因爲線程 A 執行到某一行時被掛起,還沒來得及修改lastTime
的值。比如線程 A 執行到這一行時被掛起
這時線程 B 開始執行,判斷lastTime
和nowTime
還是equals
的,線程 B 就會繼續執行並且獲得一個編號
然後線程 A 被喚起繼續執行也獲取到一個編號,這時兩個線程獲取到的編號就重複了
可以用 java 的synchronized
關鍵字把併發改爲同步
加上synchronized
後,當線程 A 在方法中正執行時,線程 B 只能在方法外等待,不能進去執行,這就解決了上面說的併發問題
但是,還有另外一個問題。
我們都知道synchronized
只針對同一個實例有效,當有多個實例時,多個實例之間無法控制
一旦產生多個實例時,多個實例之間產生的編號就有可能重複
所以我們不能讓這個類的對象產生多個實例,只能讓它始終保持只有一個實例
說到這裏,我們首先想到的就是單例模式。
單例模式最大的特點就是在任何情況下最多隻有一個實例,所以這裏使用單例模式來解決這個問題再合適不過
先說一下單例模式怎麼保證單例,要想保證單例就不能讓別人隨便創建實例。
最好的辦法就是把構造器私有化,讓它是private
的。私有化之後只有這個類自己能創建實例,其它的類都沒有調用這個類的構造器的權限
這個類只創建一個實例,那麼它就是單例的
單例模式的創建可分爲懶漢式創建和餓漢式創建
懶漢式單例模式
懶漢式從字面意思理解就是懶嘛,因爲我懶,能歇着就不會動,你沒讓我幹活我就不會主動去幹
所以,懶漢式單例模式的實例一開始爲空,等到被調用時纔會初始化
懶漢式單例模式有多種實現方式,首先我們先來看第一種
加上紅框中的內容就變成了懶漢式單例模式
但是這個單例模式在併發情況下是有可能會產生多個實例的
兩個線程獲取的實例的內存地址是不一樣的,說明獲取到的是多個實例
這是因爲在併發情況下線程 A 執行到某一行時被掛起,還沒來得及創建實例。比如下面這一行
這時線程 B 開始執行,到 18 行時判斷還沒創建實例,線程 B 就創建了一個實例
然後線程 A 被喚起,接着往下執行,也會創建一個實例
這個問題和我們剛纔講雪花算法的時候遇到的問題一樣,可以用synchronized
來解決
加上synchronized
以後,當一個線程在執行被synchronized
鎖住的代碼時,其他線程只能等待。
當這個線程執行完之後,創建了snowFlake
實例。然後別的線程才能進去執行
當別的線程進去執行的時候,發現snowFlake
不是 null 了,就不會創建新的實例了
這就解決了懶漢式單例模式在併發情況下創建多個實例的問題,但是還不夠完美
試想一下,當併發量很大的時候,因爲只有一個線程可以進去執行,其他線程只能在外面等待。
隨着訪問量越來越大,被阻塞的線程也越來越多。當阻塞的線程足夠多時,就有可能導致服務器宕機
我們可以這樣優化,在synchronized
外面再加一層非空判斷
加上外層的非空判斷之後,雖然synchronized
還是會阻塞後面過來的線程
但是,當第一個線程執行完之後,snowFlake
被實例化,不再爲 null
因爲有外層的非空判斷,所以後續的線程不會再進去執行,也不會被阻塞,而是直接 return 了
這就是一個完美的懶漢式單例模式了
餓漢式單例模式
餓漢式從字面意思理解就是餓嘛,因爲我一直餓,所以把好喫的都提前給我準備好
所以餓漢式單例模式的實例是提前創建好的,也就是類加載的時候就創建了,而不是等到用的時候再創建
我們用餓漢式單例模式來優化一下我們之前改編的雪花算法
加上紅框中的代碼雪花算法就變成了餓漢式單例模式。
紅框中第一行的snowFlake
變量是被 static 修飾的,我們都知道 static 修飾的變量是屬於這個類的,在類加載的時候就進行了初始化賦值。
而這個類只會被加載一次,所以snowFlake
變量只會被初始化一次,從而保證了單例
公衆號
源碼
下面附上餓漢式和懶漢式創建雪花算法單例模式的源碼,需要的請自取
**「餓漢式」**單例模式實現雪花算法
package com.helianxiaowu.hungrySingleton;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* @desc: 餓漢式單例模式實現雪花算法
* @author: 公衆號:赫連小伍
* @create: 2021-06-29 19:32
**/
public class SnowFlake {
private static SnowFlake snowFlake = new SnowFlake();
private SnowFlake() {}
public static SnowFlake getInstance() {
return snowFlake;
}
// 序列號,同一毫秒內用此參數來控制併發
private long sequence = 0L;
// 上一次生成編號的時間串,格式:yyMMddHHmmssSSS
private String lastTime = "";
public synchronized String getNum() {
String nowTime = getTime(); // 獲取當前時間串,格式:yyMMddHHmmssSSS
String machineId = "01"; // 機器編號,這裏假裝獲取到的機器編號是2。實際項目中可從配置文件中讀取
// 本次和上次不是同一毫秒,直接生成編號返回
if (!lastTime.equals(nowTime)) {
sequence = 0L; // 重置序列號,方便下次使用
lastTime = nowTime; // 更新時間串,方便下次使用
return new StringBuilder(nowTime).append(machineId).append(sequence).toString();
}
// 本次和上次在同一個毫秒內,需要用序列號控制併發
if (sequence < 99) { // 序列號沒有達到最大值,直接生成編號返回
sequence = sequence + 1;
return new StringBuilder(nowTime).append(machineId).append(sequence).toString();
}
// 序列號達到最大值,需要等待下一毫秒的到來
while (lastTime.equals(nowTime)) {
nowTime = getTime();
}
sequence = 0L; // 重置序列號,方便下次使用
lastTime = nowTime; // 更新時間串,方便下次使用
return new StringBuilder(nowTime).append(machineId).append(sequence).toString();
}
private String getTime() {
return LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyMMddHHmmssSSS"));
}
}
**「懶漢式」**單例模式實現雪花算法
package com.helianxiaowu.lazySingleton;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* @desc: 懶漢式單例模式實現雪花算法
* @author: 公衆號:赫連小伍
* @create: 2021-06-29 19:32
**/
public class SnowFlake {
private static SnowFlake snowFlake = null;
private SnowFlake() {}
public static SnowFlake getInstance() {
if (snowFlake == null) {
synchronized (SnowFlake.class) {
if (snowFlake == null) {
snowFlake = new SnowFlake();
}
return snowFlake;
}
}
return snowFlake;
}
// 序列號,同一毫秒內用此參數來控制併發
private long sequence = 0L;
// 上一次生成編號的時間串,格式:yyMMddHHmmssSSS
private String lastTime = "";
public synchronized String getNum() {
String nowTime = getTime(); // 獲取當前時間串,格式:yyMMddHHmmssSSS
String machineId = "01"; // 機器編號,這裏假裝獲取到的機器編號是2。實際項目中可從配置文件中讀取
// 本次和上次不是同一毫秒,直接生成編號返回
if (!lastTime.equals(nowTime)) {
sequence = 0L; // 重置序列號,方便下次使用
lastTime = nowTime; // 更新時間串,方便下次使用
return new StringBuilder(nowTime).append(machineId).append(sequence).toString();
}
// 本次和上次在同一個毫秒內,需要用序列號控制併發
if (sequence < 99) { // 序列號沒有達到最大值,直接生成編號返回
sequence = sequence + 1;
return new StringBuilder(nowTime).append(machineId).append(sequence).toString();
}
// 序列號達到最大值,需要等待下一毫秒的到來
while (lastTime.equals(nowTime)) {
nowTime = getTime();
}
sequence = 0L; // 重置序列號,方便下次使用
lastTime = nowTime; // 更新時間串,方便下次使用
return new StringBuilder(nowTime).append(machineId).append(sequence).toString();
}
private String getTime() {
return LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyMMddHHmmssSSS"));
}
}
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/d1vhdXmRqnM26RJJRJeiTQ