用单例模式实现雪花算法

1,196 阅读12分钟

    雪花算法

    雪花算法适用于生成全局唯一的编号,比如数据库主键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"));
        }
    }