你的唯一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


 

                                              文辞粗浅,不当之处请指教,如果觉得文章不错,请关注或点赞  (-__-)谢谢

 

 

 

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