单例模式实现雪花算法

导读:本篇文章讲解 单例模式实现雪花算法,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

雪花算法

雪花算法适用于生成全局唯一的编号,比如数据库主键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只针对同一个实例有效,当有多个实例时,多个实例之间无法控制

图片

一旦产生多个实例时,多个实例之间产生的编号就有可能重复

所以我们不能让这个类的对象产生多个实例,只能让它始终保持只有一个实例

说到这里,我们首先想到的就是单例模式。另外,设计模式系列面试题和答案全部整理好了,微信搜索Java技术栈,在后台发送:面试,可以在线阅读。

单例模式最大的特点就是在任何情况下最多只有一个实例,所以这里使用单例模式来解决这个问题再合适不过

先说一下单例模式怎么保证单例,要想保证单例就不能让别人随便创建实例。

最好的办法就是把构造器私有化,让它是private的。私有化之后只有这个类自己能创建实例,其它的类都没有调用这个类的构造器的权限

这个类只创建一个实例,那么它就是单例的

单例模式的创建可分为懒汉式创建和饿汉式创建

懒汉式单例模式

懒汉式从字面意思理解就是懒嘛,因为我懒,能歇着就不会动,你没让我干活我就不会主动去干

所以,懒汉式单例模式的实例一开始为空,等到被调用时才会初始化

懒汉式单例模式有多种实现方式,首先我们先来看第一种

图片

加上红框中的内容就变成了懒汉式单例模式

但是这个单例模式在并发情况下是有可能会产生多个实例的

图片

两个线程获取的实例的内存地址是不一样的,说明获取到的是多个实例

这是因为在并发情况下线程A执行到某一行时被挂起,还没来得及创建实例。比如下面这一行

图片

这时线程B开始执行,到18行时判断还没创建实例,线程B就创建了一个实例

然后线程A被唤起,接着往下执行,也会创建一个实例

这个问题和我们刚才讲雪花算法的时候遇到的问题一样,可以用synchronized来解决

单例模式实现雪花算法

加上synchronized以后,当一个线程在执行被synchronized锁住的代码时,其他线程只能等待。

当这个线程执行完之后,创建了snowFlake实例。然后别的线程才能进去执行

当别的线程进去执行的时候,发现snowFlake不是null了,就不会创建新的实例了

这就解决了懒汉式单例模式在并发情况下创建多个实例的问题,但是还不够完美

试想一下,当并发量很大的时候,因为只有一个线程可以进去执行,其他线程只能在外面等待。

随着访问量越来越大,被阻塞的线程也越来越多。当阻塞的线程足够多时,就有可能导致服务器宕机

我们可以这样优化,在synchronized外面再加一层非空判断

图片

加上外层的非空判断之后,虽然synchronized还是会阻塞后面过来的线程

但是,当第一个线程执行完之后,snowFlake被实例化,不再为null

因为有外层的非空判断,所以后续的线程不会再进去执行,也不会被阻塞,而是直接return了

这就是一个完美的懒汉式单例模式了

饿汉式单例模式

饿汉式从字面意思理解就是饿嘛,因为我一直饿,所以把好吃的都提前给我准备好

所以饿汉式单例模式的实例是提前创建好的,也就是类加载的时候就创建了,而不是等到用的时候再创建

我们用饿汉式单例模式来优化一下我们之前改编的雪花算法

图片

加上红框中的代码雪花算法就变成了饿汉式单例模式。

红框中第一行的snowFlake变量是被static修饰的,我们都知道static修饰的变量是属于这个类的,在类加载的时候就进行了初始化赋值。

而这个类只会被加载一次,所以snowFlake变量只会被初始化一次,从而保证了单例。

推荐:单例模式的 8 种写法,非常全!

源码

下面附上饿汉式和懒汉式创建雪花算法单例模式的源码,需要的请自取

「饿汉式」单例模式实现雪花算法

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"));
    }
}

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/111057.html

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!