关于后端:雪花算法对SystemcurrentTimeMillis优化真的有用么

39次阅读

共计 6765 个字符,预计需要花费 17 分钟才能阅读完成。

后面曾经讲过了雪花算法,外面应用了 System.currentTimeMillis() 获取工夫,有一种说法是认为 System.currentTimeMillis() 慢,是因为每次调用都会去跟零碎打一次交道,在高并发状况下,大量并发的零碎调用容易会影响性能(对它的调用甚至比 new 一个一般对象都要耗时,毕竟 new 产生的对象只是在 Java 内存中的堆中)。咱们能够看到它调用的是native 办法:

// 返回以后工夫,以毫秒为单位。留神,尽管返回值的工夫单位是毫秒,但值的粒度取决于底层操作系统,可能更大。例如,许多操作系统以数十毫秒为单位度量工夫。public static native long currentTimeMillis();

所以有人提议,用后盾线程定时去更新时钟,并且是单例的,防止每次都与零碎打交道,也防止了频繁的线程切换,这样或者能够提高效率。

这个优化成立么?

先上优化代码:

package snowflake;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

public class SystemClock {

    private final int period;

    private final AtomicLong now;

    private static final SystemClock INSTANCE = new SystemClock(1);

    private SystemClock(int period) {
        this.period = period;
        now = new AtomicLong(System.currentTimeMillis());
        scheduleClockUpdating();}

    private void scheduleClockUpdating() {ScheduledExecutorService scheduleService = Executors.newSingleThreadScheduledExecutor((r) -> {Thread thread = new Thread(r);
            thread.setDaemon(true);
            return thread;
        });
        scheduleService.scheduleAtFixedRate(() -> {now.set(System.currentTimeMillis());
        }, 0, period, TimeUnit.MILLISECONDS);
    }

    private long get() {return now.get();
    }

    public static long now() {return INSTANCE.get();
    }

}

只须要用 SystemClock.now() 替换 System.currentTimeMillis() 即可。

雪花算法 SnowFlake 的代码也放在这里:

package snowflake;

public class SnowFlake {// 数据中心(机房) id
    private long datacenterId;
    // 机器 ID
    private long workerId;
    // 同一时间的序列
    private long sequence;

    public SnowFlake(long workerId, long datacenterId) {this(workerId, datacenterId, 0);
    }

    public SnowFlake(long workerId, long datacenterId, long sequence) {
        // 非法判断
        if (workerId > maxWorkerId || workerId < 0) {throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
        }
        if (datacenterId > maxDatacenterId || datacenterId < 0) {throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
        }
        System.out.printf("worker starting. timestamp left shift %d, datacenter id bits %d, worker id bits %d, sequence bits %d, workerid %d",
                timestampLeftShift, datacenterIdBits, workerIdBits, sequenceBits, workerId);

        this.workerId = workerId;
        this.datacenterId = datacenterId;
        this.sequence = sequence;
    }

    // 开始工夫戳(2021-10-16 22:03:32)private long twepoch = 1634393012000L;

    // 机房号,的 ID 所占的位数 5 个 bit 最大:11111(2 进制)--> 31(10 进制)
    private long datacenterIdBits = 5L;

    // 机器 ID 所占的位数 5 个 bit 最大:11111(2 进制)--> 31(10 进制)
    private long workerIdBits = 5L;

    // 5 bit 最多只能有 31 个数字,就是说机器 id 最多只能是 32 以内
    private long maxWorkerId = -1L ^ (-1L << workerIdBits);

    // 5 bit 最多只能有 31 个数字,机房 id 最多只能是 32 以内
    private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);

    // 同一时间的序列所占的位数 12 个 bit 111111111111 = 4095  最多就是同一毫秒生成 4096 个
    private long sequenceBits = 12L;

    // workerId 的偏移量
    private long workerIdShift = sequenceBits;

    // datacenterId 的偏移量
    private long datacenterIdShift = sequenceBits + workerIdBits;

    // timestampLeft 的偏移量
    private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;

    // 序列号掩码 4095 (0b111111111111=0xfff=4095)
    // 用于序号的与运算,保障序号最大值在 0 -4095 之间
    private long sequenceMask = -1L ^ (-1L << sequenceBits);

    // 最近一次工夫戳
    private long lastTimestamp = -1L;


    // 获取机器 ID
    public long getWorkerId() {return workerId;}


    // 获取机房 ID
    public long getDatacenterId() {return datacenterId;}


    // 获取最新一次获取的工夫戳
    public long getLastTimestamp() {return lastTimestamp;}


    // 获取下一个随机的 ID
    public synchronized long nextId() {
        // 获取以后工夫戳,单位毫秒
        long timestamp = timeGen();

        if (timestamp < lastTimestamp) {System.err.printf("clock is moving backwards.  Rejecting requests until %d.", lastTimestamp);
            throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds",
                    lastTimestamp - timestamp));
        }

        // 去重
        if (lastTimestamp == timestamp) {sequence = (sequence + 1) & sequenceMask;

            // sequence 序列大于 4095
            if (sequence == 0) {
                // 调用到下一个工夫戳的办法
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            // 如果是以后工夫的第一次获取,那么就置为 0
            sequence = 0;
        }

        // 记录上一次的工夫戳
        lastTimestamp = timestamp;

        // 偏移计算
        return ((timestamp - twepoch) << timestampLeftShift) |
                (datacenterId << datacenterIdShift) |
                (workerId << workerIdShift) |
                sequence;
    }

    private long tilNextMillis(long lastTimestamp) {
        // 获取最新工夫戳
        long timestamp = timeGen();
        // 如果发现最新的工夫戳小于或者等于序列号曾经超 4095 的那个工夫戳
        while (timestamp <= lastTimestamp) {
            // 不合乎则持续
            timestamp = timeGen();}
        return timestamp;
    }

    private long timeGen() {return SystemClock.now();
        // return System.currentTimeMillis();}

    public static void main(String[] args) {SnowFlake worker = new SnowFlake(1, 1);
        long timer = System.currentTimeMillis();
        for (int i = 0; i < 10000000; i++) {worker.nextId();
        }
        System.out.println(System.currentTimeMillis());
        System.out.println(System.currentTimeMillis() - timer);
    }
}

Windows:i5-4590 16G 内存 4 核 512 固态

Mac: Mac pro 2020 512G 固态 16G 内存

Linux:deepin 零碎,虚拟机,160G 磁盘,内存 8G

单线程环境测试一下 System.currentTimeMillis()

平台 / 数据量 10000 1000000 10000000 100000000
mac 5 247 2444 24416
windows 3 249 2448 24426
linux(deepin) 135 598 4076 26388

单线程环境测试一下 SystemClock.now()

平台 / 数据量 10000 1000000 10000000 100000000
mac 52 299 2501 24674
windows 56 3942 38934 389983
linux(deepin) 336 1226 4454 27639

下面的单线程测试并没有体现出后盾时钟线程解决的劣势,反而在 windows 下,数据量大的时候,变得异样的慢,linux 零碎上,也并没有快,反而变慢了一点。

多线程测试代码:

    public static void main(String[] args) throws InterruptedException {
        int threadNum = 16;
        CountDownLatch countDownLatch = new CountDownLatch(threadNum);
        int num = 100000000 / threadNum;
        long timer = System.currentTimeMillis();
        thread(num, countDownLatch);
        countDownLatch.await();
        System.out.println(System.currentTimeMillis() - timer);

    }

    public static void thread(int num, CountDownLatch countDownLatch) {List<Thread> threadList = new ArrayList<>();
        for (int i = 0; i < countDownLatch.getCount(); i++) {Thread cur = new Thread(new Runnable() {
                @Override
                public void run() {SnowFlake worker = new SnowFlake(1, 1);
                    for (int i = 0; i < num; i++) {worker.nextId();
                    }
                    countDownLatch.countDown();}
            });
            threadList.add(cur);
        }
        for (Thread t : threadList) {t.start();
        }
    }

上面咱们用不同线程数来测试 100000000(一亿) 数据量 System.currentTimeMillis()

平台 / 线程 2 4 8 16
mac 14373 6132 3410 3247
windows 12408 6862 6791 7114
linux 20753 19055 18919 19602

用不同线程数来测试 100000000(一亿) 数据量 SystemClock.now()

平台 / 线程 2 4 8 16
mac 12319 6275 3691 3746
windows 194763 110442 153960 174974
linux 26516 25313 25497 25544

在多线程的状况下,咱们能够看到 mac 上没有什么太大变动,随着线程数减少,速度还变快了,直到超过 8 的时候,然而 windows 上显著变慢了,测试的时候我都开始刷起了小视频,才跑进去后果。而且这个数据和处理器的外围也是相干的,当 windows 的线程数超过了 4 之后,就变慢了,起因是我的机器只有四核,超过了就会产生很多上下文切换的状况。

linux 上因为虚拟机,核数减少的时候,并无太多作用,然而工夫比照于间接调用 System.currentTimeMillis()其实是变慢的。

然而还有个问题,到底不同办法调用,工夫反复的概率哪一个大呢?

    static AtomicLong atomicLong = new AtomicLong(0);
    private long timeGen() {atomicLong.incrementAndGet();
        // return SystemClock.now();
        return System.currentTimeMillis();}

上面是 1 千万 id,八个线程,测进去调用 timeGen() 的次数,也就是能够看出工夫抵触的次数:

平台 / 办法 SystemClock.now() System.currentTimeMillis()
mac 23067209 12896314
windows 705460039 35164476
linux 1165552352 81422626

能够看出的确 SystemClock.now() 本人保护工夫,获取的工夫雷同的可能性更大,会触发更多次数的反复调用,抵触次数变多,这个是不利因素!还有一个残暴的事实,那就是本人定义的后盾工夫刷新,获取的工夫不是那么的精确。在 linux 中的这个差距就更大了,工夫抵触次数太多了。

后果

理论测试下来,并没有发现 SystemClock.now() 可能优化很大的效率,反而会因为竞争,获取工夫抵触的可能性更大。JDK开发人员真的不傻,他们应该也通过了很长时间的测试,比咱们本人的测试靠谱得多,因而,个人观点,最终证实这个优化并不是那么的牢靠。

不要轻易置信某一个论断,如果有疑难,请肯定做做试验,或者找足够权威的说法。

【作者简介】
秦怀,公众号【秦怀杂货店 】作者,技术之路不在一时,山高水长,纵使迟缓,驰而不息。集体写作方向:Java 源码解析JDBCMybatisSpringredis 分布式 剑指 OfferLeetCode等,认真写好每一篇文章,不喜爱题目党,不喜爱花里胡哨,大多写系列文章,不能保障我写的都完全正确,然而我保障所写的均通过实际或者查找材料。脱漏或者谬误之处,还望斧正。

剑指 Offer 全副题解 PDF

2020 年我写了什么?

开源编程笔记

正文完
 0