雪花算法對System.currentTimeMillis()優化真的有用么?


前面已經講過了雪花算法,里面使用了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年我寫了什么?

開源編程筆記


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM