你的唯一ID生成器適用於多線程嗎

完整代碼從github獲取:多線程創建唯一ID

昨天逛博客,看到一篇"Java生成唯一ID"的文章,轉載率很高。正好前段時間項目也遇到了多線程情況下唯一鍵重複的問題,正好學習一波大神的代碼,驗證一波大佬的代碼是不是適用於多線程!

代碼拷貝自Java中生成唯一ID的方法中的Snowflake算法的變化.

public class MinuteCounter {
    private static final int MASK = 0x7FFFFFFF;
    private final AtomicInteger atom;
    
    public MinuteCounter() {
        atom = new AtomicInteger(0);
    }
    
    public final int incrementAndGet() {
        return atom.incrementAndGet() & MASK;
    }
    
    public int get() {
        return atom.get() & MASK;
    }
    
    public void set(int newValue) {
        atom.set(newValue & MASK);
    }
}

 

/**
 * @ClassName: SnowflakeIdWorker3rd
 * @Description:snowflake算法改進
 * @author: wanghao
 * @date: 2019年12月13日 下午12:50:47
 * @version V1.0
 * 
 *          將產生的Id類型更改爲Integer 32bit <br>
 *          把時間戳的單位改爲分鐘,使用25個比特的時間戳(分鐘) <br>
 *          去掉機器ID和數據中心ID <br> 
 *          7個比特作爲自增值,即2的7次方等於128。
 */
public class SnowflakeIdWorker3rd {
    /** 開始時間戳 (2019-01-01) */
    private final int twepoch = 25771200;// 1546272000000L/1000/60;

    /** 序列在id中佔的位數 */
    private final long sequenceBits = 7L;

    /** 時間截向左移7位 */
    private final long timestampLeftShift = sequenceBits;

    /** 生成序列的掩碼,這裏爲127 */
    private final int sequenceMask = -1 ^ (-1 << sequenceBits);

    /** 分鐘內序列(0~127) */
    private int sequence = 0;
    private int laterSequence = 0;

    /** 上次生成ID的時間戳 */
    private int lastTimestamp = -1;

    private final MinuteCounter counter = new MinuteCounter();
    
    /** 預支時間標誌位 */
    boolean isAdvance = false;

    // ==============================Constructors=====================================
    public SnowflakeIdWorker3rd() {

    }

    // ==============================Methods==========================================
    /**
     * 獲得下一個ID (該方法是線程安全的)
     * 
     * @return SnowflakeId
     */
    public synchronized int nextId() {
        
        
        int timestamp = timeGen();
        // 如果當前時間小於上一次ID生成的時間戳,說明系統時鐘回退過這個時候應當拋出異常
        if (timestamp < lastTimestamp) {
            throw new RuntimeException(String.format(
                    "Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
        }
        
        if(timestamp > counter.get()) {
            counter.set(timestamp);
            isAdvance = false;
        }

        // 如果是同一時間生成的,則進行分鐘內序列
        if (lastTimestamp == timestamp || isAdvance) {
            if(!isAdvance) {
                sequence = (sequence + 1) & sequenceMask;
            }

            // 分鐘內自增列溢出
            if (sequence == 0) {
                // 預支下一個分鐘,獲得新的時間戳
                isAdvance = true;
                int laterTimestamp = counter.get();
                if (laterSequence == 0) {
                    laterTimestamp = counter.incrementAndGet();
                }

                int nextId = ((laterTimestamp - twepoch) << timestampLeftShift) //
                        | laterSequence;
                laterSequence = (laterSequence + 1) & sequenceMask;
                return nextId;
            }
        }
        // 時間戳改變,分鐘內序列重置
        else {
            sequence = 0;
            laterSequence = 0;
        }

        // 上次生成ID的時間截
        lastTimestamp = timestamp;

        // 移位並通過或運算拼到一起組成32位的ID
        return ((timestamp - twepoch) << timestampLeftShift) //
                | sequence;
    }

    /**
     * 返回以分鐘爲單位的當前時間
     * 
     * @return 當前時間(分鐘)
     */
    protected int timeGen() {
        String timestamp = String.valueOf(System.currentTimeMillis() / 1000 / 60);
        return Integer.valueOf(timestamp);
    }

    // ==============================Test=============================================
    /** 測試 */
    public static void main(String[] args) {
        SnowflakeIdWorker3rd idWorker = new SnowflakeIdWorker3rd();
        for (int i = 0; i < 1000; i++) {
            long id = idWorker.nextId();
            System.out.println(i + ": " + id);
        }
    }
}

拷貝、粘貼、運行main方法,一波操作後控制檯輸出了1000個整整齊齊的ID,香~

寫一個CountDownLatch測測多線程場景,沒有問題就拿到項目裏裝逼了

說明:CountDownLatch能保證多個線程同時執行,較大程度還原實際併發場景使用CountDownLatch模擬併發

public class TestThread extends Thread {

    public static List<Long> idList = null;
    public static void main(String[] args) throws InterruptedException {
        idList = new ArrayList<Long>();
        final CountDownLatch latch = new CountDownLatch(1);
        for(int i = 0 ; i < 2 ;i ++ ){
            Thread thread = new TestThread(latch,i);
            thread.start();
        }
        Thread.sleep(5000);    //延時2秒
        System.out.println(idList);
        System.out.println("去重前ID數量:"+idList.size());
        idList = idList.stream().distinct().collect(Collectors.toList());
        System.out.println("去重後ID數量:"+idList.size());
    }
    private CountDownLatch latch;
    private int num;
    public TestThread(CountDownLatch latch,int num) {
        this.latch = latch;
        this.num = num;
    }
    @Override
    public void run() {
        SnowflakeIdWorker3rd idWorker = new SnowflakeIdWorker3rd();
        latch.countDown();
        try {
            latch.await();
            for (int i = 0; i < 5; i++) {
                long id = idWorker.nextId();
                idList.add(id);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

這段代碼的意思是啓2個線程,每個線程生成5個ID,預期生成10個互不重複的ID,並把生成的ID去重前後的數量分別打印出來。

問題出現了,每次生成的ID都會有五個重複

再翻看一下大佬的代碼,問題找到了!代碼中synchronized關鍵字修飾了nextId()方法來確保不被重複調用,但是(敲黑板):修飾方法時鎖定的是調用該方法的對象,它並不能使調用該方法的多個對象在執行順序上互斥。所以當有多個線程都實例化了SnowflakeIdWorker3rd類並調用nextId方法,此時不能保證數據數據唯一。


對代碼進行簡單改造

改造方案是確保JVM中只有一個SnowflakeIdWorker3rd實例,爲SnowflakeIdWorker3rd建一個統一創建Id的方法createId(),並把nextId改爲private修飾,避免被其他人誤調。

改造完成再試試,結果符合預期。(數據缺、有值爲null是因爲list是線程不安全的)

 生成唯一ID這種事一定要考慮多線程情況,不然出現了數據重複就打臉了!

完整代碼從github獲取:多線程創建唯一ID


 

                                              文辭粗淺,不當之處請指教,如果覺得文章不錯,請關注或點贊  (-__-)謝謝

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章