共计 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 源码解析
,JDBC
,Mybatis
,Spring
,redis
, 分布式
, 剑指 Offer
,LeetCode
等,认真写好每一篇文章,不喜爱题目党,不喜爱花里胡哨,大多写系列文章,不能保障我写的都完全正确,然而我保障所写的均通过实际或者查找材料。脱漏或者谬误之处,还望斧正。
剑指 Offer 全副题解 PDF
2020 年我写了什么?
开源编程笔记