スノーフレークアルゴリズムではID生成時にSystem.currentTimeMillis()を使用して現在時刻を取得します。このメソッドは毎回システムコールを発行するため、高並行环境下ではパフォーマンスに影響を与える可能性があるという意見があります。通常のオブジェクト生成よりも時間がかかる場合があるのは、Javaヒープ内での操作ではなく、ネイティブメソッド呼び出しが含まれるためです。
// 現在時刻をミリ秒単位で返す。戻り値の単位はミリ秒だが、
// 精度は基礎となるOSに依存し、より粗い場合がある
public static native long currentTimeMillis();
この課題に対し、バックグラウンドスレッドで定期的に時刻を更新する単一インスタンス時計を実装することで、システムコールを削減し性能向上を図るという最適化案が提唱されました。
この最適化は本当に効果があるのでしょうか。
最適化コードの実装例:
package timestamp;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
public class CachedTimer {
private final int refreshInterval;
private final AtomicLong cachedTime;
private static final CachedTimer INSTANCE = new CachedTimer(1);
private CachedTimer(int interval) {
this.refreshInterval = interval;
this.cachedTime = new AtomicLong(System.currentTimeMillis());
initializePeriodicUpdate();
}
private void initializePeriodicUpdate() {
ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(runnable -> {
Thread thread = new Thread(runnable);
thread.setDaemon(true);
thread.setName("CachedTimer-Updater");
return thread;
});
executor.scheduleAtFixedRate(() -> {
cachedTime.set(System.currentTimeMillis());
}, 0, refreshInterval, TimeUnit.MILLISECONDS);
}
private long fetchTime() {
return cachedTime.get();
}
public static long currentTime() {
return INSTANCE.fetchTime();
}
}
スノーフレークアルゴリズムの実装例:
package idgenerator;
public class SnowflakeIdGenerator {
private final long machineId;
private final long datacenterId;
private long sequence;
public SnowflakeIdGenerator(long machineId, long datacenterId) {
this(machineId, datacenterId, 0);
}
public SnowflakeIdGenerator(long machineId, long datacenterId, long initialSequence) {
validateIds(machineId, datacenterId);
this.machineId = machineId;
this.datacenterId = datacenterId;
this.sequence = initialSequence;
}
private final long epoch = 1634393012000L;
private final long machineIdBits = 5L;
private final long datacenterIdBits = 5L;
private final long sequenceBits = 12L;
private final long maxMachineId = -1L ^ (-1L << machineIdBits);
private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
private final long sequenceMask = -1L ^ (-1L << sequenceBits);
private final long machineIdShift = sequenceBits;
private final long datacenterIdShift = sequenceBits + machineIdBits;
private final long timestampShift = sequenceBits + machineIdBits + datacenterIdBits;
private long lastTimestamp = -1L;
private void validateIds(long machineId, long datacenterId) {
if (machineId > maxMachineId || machineId < 0) {
throw new IllegalArgumentException("Machine ID must be between 0 and " + maxMachineId);
}
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException("Datacenter ID must be between 0 and " + maxDatacenterId);
}
}
public synchronized long generateId() {
long currentTimestamp = getCurrentTimestamp();
if (currentTimestamp < lastTimestamp) {
throw new RuntimeException("Clock moved backwards. Refusing to generate ID.");
}
if (currentTimestamp == lastTimestamp) {
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
currentTimestamp = waitNextMillis(lastTimestamp);
}
} else {
sequence = 0;
}
lastTimestamp = currentTimestamp;
return ((currentTimestamp - epoch) << timestampShift) |
(datacenterId << datacenterIdShift) |
(machineId << machineIdShift) |
sequence;
}
private long waitNextMillis(long lastTimestamp) {
long timestamp = getCurrentTimestamp();
while (timestamp <= lastTimestamp) {
timestamp = getCurrentTimestamp();
}
return timestamp;
}
private long getCurrentTimestamp() {
return CachedTimer.currentTime();
// return System.currentTimeMillis();
}
}
テスト環境:
- Windows: i5-4590 16GB RAM 4コア 512GB SSD
- Mac: Mac Pro 2020 512GB SSD 16GB RAM
- Linux: Deepin OS、仮想マシン、160GBディスク、8GB RAM
単一スレッド環境でのSystem.currentTimeMillis()パフォーマンス:
| プラットフォーム/件数 | 10,000 | 1,000,000 | 10,000,000 | 100,000,000 |
|---|---|---|---|---|
| Mac | 5 | 247 | 2444 | 24416 |
| Windows | 3 | 249 | 2448 | 24426 |
| Linux | 135 | 598 | 4076 | 26388 |
単一スレッド環境でのCachedTimer.currentTime()パフォーマンス:
| プラットフォーム/件数 | 10,000 | 1,000,000 | 10,000,000 | 100,000,000 |
|---|---|---|---|---|
| Mac | 52 | 299 | 2501 | 24674 |
| Windows | 56 | 3942 | 38934 | 389983 |
| Linux | 336 | 1226 | 4454 | 27639 |
単一スレッドテストでは、バックグラウンド時計の利点が現れず、Windows環境では大量データ処理時に著しく遅くなりました。Linux環境でも改善は見られませんでした。
マルチスレッドテストコード:
public class PerformanceTest {
public static void main(String[] args) throws InterruptedException {
int threadCount = 16;
CountDownLatch latch = new CountDownLatch(threadCount);
int idsPerThread = 100000000 / threadCount;
long startTime = System.currentTimeMillis();
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
SnowflakeIdGenerator generator = new SnowflakeIdGenerator(1, 1);
for (int j = 0; j < idsPerThread; j++) {
generator.generateId();
}
latch.countDown();
}).start();
}
latch.await();
System.out.println("Total time: " + (System.currentTimeMillis() - startTime));
}
}
マルチスレッド環境でのSystem.currentTimeMillis()パフォーマンス(100,000,000件):
| プラットフォーム/スレッド数 | 2 | 4 | 8 | 16 |
|---|---|---|---|---|
| Mac | 14373 | 6132 | 3410 | 3247 |
| Windows | 12408 | 6862 | 6791 | 7114 |
| Linux | 20753 | 19055 | 18919 | 19602 |
マルチスレッド環境でのCachedTimer.currentTime()パフォーマンス(100,000,000件):
| プラットフォーム/スレッド数 | 2 | 4 | 8 | 16 |
|---|---|---|---|---|
| Mac | 12319 | 6275 | 3691 | 3746 |
| Windows | 194763 | 110442 | 153960 | 174974 |
| Linux | 26516 | 25313 | 25497 | 25544 |
マルチスレッド環境では、Macでは大きな変化はありませんでしたが、Windowsでは顕著な性能低下が見られました。LinuxでもSystem.currentTimeMillis()に比べて遅くなっています。
時刻衝突の頻度も重要な指標です:
static AtomicLong collisionCounter = new AtomicLong(0);
private long getCurrentTimestamp() {
collisionCounter.incrementAndGet();
return CachedTimer.currentTime();
// return System.currentTimeMillis();
}
10,000,000件のID生成を8スレッドで実行した際の時刻取得回数:
| プラットフォーム/メソッド | CachedTimer | System.currentTimeMillis |
|---|---|---|
| Mac | 23067209 | 12896314 |
| Windows | 705460039 | 35164476 |
| Linux | 1165552352 | 81422626 |
キャッシュされた時刻を使用する場合、同じ時刻が取得される可能性が高く、時刻衝突が頻繁に発生します。特にLinux環境では衝突回数が著しく増加しています。
結論として、実際のテストではCachedTimer.currentTime()による最適化効果は確認できず、競合により時刻衝突の可能性が増加するだけでした。JDK開発者は長期間にわたるテストと最適化を行っており、彼らの実装は信頼性が高いと言えます。結論として、この最適化手法は有効とは言えません。
技術的な判断を行う際は、主観的な意見に頼らず、実験的な検証や信頼できる情報源を参考にすることが重要です。