前面已經講過了雪花演算法,裡面使用了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原始碼解析
,JDBC
,Mybatis
,Spring
,redis
,分散式
,劍指Offer
,LeetCode
等,認真寫好每一篇文章,不喜歡標題黨,不喜歡花裡胡哨,大多寫系列文章,不能保證我寫的都完全正確,但是我保證所寫的均經過實踐或者查詢資料。遺漏或者錯誤之處,還望指正。
劍指Offer全部題解PDF
2020年我寫了什麼?
開源程式設計筆記