你的唯一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 
* 把时间戳的单位改为分钟,使用25个比特的时间戳(分钟)
* 去掉机器ID和数据中心ID
* 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,香~

你的唯一ID生成器适用于多线程吗_第1张图片

写一个CountDownLatch测测多线程场景,没有问题就拿到项目里装逼了

说明:CountDownLatch能保证多个线程同时执行,较大程度还原实际并发场景使用CountDownLatch模拟并发

public class TestThread extends Thread {

    public static List idList = null;
    public static void main(String[] args) throws InterruptedException {
        idList = new ArrayList();
        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都会有五个重复

你的唯一ID生成器适用于多线程吗_第2张图片

再翻看一下大佬的代码,问题找到了!代码中synchronized关键字修饰了nextId()方法来确保不被重复调用,但是(敲黑板):修饰方法时锁定的是调用该方法的对象,它并不能使调用该方法的多个对象在执行顺序上互斥。所以当有多个线程都实例化了SnowflakeIdWorker3rd类并调用nextId方法,此时不能保证数据数据唯一。


对代码进行简单改造

改造方案是确保JVM中只有一个SnowflakeIdWorker3rd实例,为SnowflakeIdWorker3rd建一个统一创建Id的方法createId(),并把nextId改为private修饰,避免被其他人误调。

你的唯一ID生成器适用于多线程吗_第3张图片

改造完成再试试,结果符合预期。(数据缺、有值为null是因为list是线程不安全的)

 生成唯一ID这种事一定要考虑多线程情况,不然出现了数据重复就打脸了!

完整代码从github获取:多线程创建唯一ID


 

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

你的唯一ID生成器适用于多线程吗_第4张图片

 

 

 

你可能感兴趣的:(Java)