美女同事用單例模式花式實現雪花算法

雪花算法適用於生成全局唯一的編號,比如數據庫主鍵 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 開始執行,判斷lastTimenowTime還是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