完整代碼從github獲取:多線程創建唯一ID
昨天逛博客,看到一篇"Java生成唯一ID"的文章,轉載率很高。正好前段時間項目也遇到了多線程情況下唯一鍵重複的問題,正好學習一波大神的代碼,驗證一波大佬的代碼是不是適用於多線程!
代碼拷貝自Java中生成唯一ID的方法中的Snowflake算法的變化.
public class MinuteCounter {
private static final int MASK = 0x7FFFFFFF;
private final AtomicInteger atom;
public MinuteCounter() {
atom = new AtomicInteger(0);
}
public final int incrementAndGet() {
return atom.incrementAndGet() & MASK;
}
public int get() {
return atom.get() & MASK;
}
public void set(int newValue) {
atom.set(newValue & MASK);
}
}
/**
* @ClassName: SnowflakeIdWorker3rd
* @Description:snowflake算法改進
* @author: wanghao
* @date: 2019年12月13日 下午12:50:47
* @version V1.0
*
* 將產生的Id類型更改爲Integer 32bit <br>
* 把時間戳的單位改爲分鐘,使用25個比特的時間戳(分鐘) <br>
* 去掉機器ID和數據中心ID <br>
* 7個比特作爲自增值,即2的7次方等於128。
*/
public class SnowflakeIdWorker3rd {
/** 開始時間戳 (2019-01-01) */
private final int twepoch = 25771200;// 1546272000000L/1000/60;
/** 序列在id中佔的位數 */
private final long sequenceBits = 7L;
/** 時間截向左移7位 */
private final long timestampLeftShift = sequenceBits;
/** 生成序列的掩碼,這裏爲127 */
private final int sequenceMask = -1 ^ (-1 << sequenceBits);
/** 分鐘內序列(0~127) */
private int sequence = 0;
private int laterSequence = 0;
/** 上次生成ID的時間戳 */
private int lastTimestamp = -1;
private final MinuteCounter counter = new MinuteCounter();
/** 預支時間標誌位 */
boolean isAdvance = false;
// ==============================Constructors=====================================
public SnowflakeIdWorker3rd() {
}
// ==============================Methods==========================================
/**
* 獲得下一個ID (該方法是線程安全的)
*
* @return SnowflakeId
*/
public synchronized int nextId() {
int timestamp = timeGen();
// 如果當前時間小於上一次ID生成的時間戳,說明系統時鐘回退過這個時候應當拋出異常
if (timestamp < lastTimestamp) {
throw new RuntimeException(String.format(
"Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
}
if(timestamp > counter.get()) {
counter.set(timestamp);
isAdvance = false;
}
// 如果是同一時間生成的,則進行分鐘內序列
if (lastTimestamp == timestamp || isAdvance) {
if(!isAdvance) {
sequence = (sequence + 1) & sequenceMask;
}
// 分鐘內自增列溢出
if (sequence == 0) {
// 預支下一個分鐘,獲得新的時間戳
isAdvance = true;
int laterTimestamp = counter.get();
if (laterSequence == 0) {
laterTimestamp = counter.incrementAndGet();
}
int nextId = ((laterTimestamp - twepoch) << timestampLeftShift) //
| laterSequence;
laterSequence = (laterSequence + 1) & sequenceMask;
return nextId;
}
}
// 時間戳改變,分鐘內序列重置
else {
sequence = 0;
laterSequence = 0;
}
// 上次生成ID的時間截
lastTimestamp = timestamp;
// 移位並通過或運算拼到一起組成32位的ID
return ((timestamp - twepoch) << timestampLeftShift) //
| sequence;
}
/**
* 返回以分鐘爲單位的當前時間
*
* @return 當前時間(分鐘)
*/
protected int timeGen() {
String timestamp = String.valueOf(System.currentTimeMillis() / 1000 / 60);
return Integer.valueOf(timestamp);
}
// ==============================Test=============================================
/** 測試 */
public static void main(String[] args) {
SnowflakeIdWorker3rd idWorker = new SnowflakeIdWorker3rd();
for (int i = 0; i < 1000; i++) {
long id = idWorker.nextId();
System.out.println(i + ": " + id);
}
}
}
拷貝、粘貼、運行main方法,一波操作後控制檯輸出了1000個整整齊齊的ID,香~
寫一個CountDownLatch測測多線程場景,沒有問題就拿到項目裏裝逼了
說明:CountDownLatch能保證多個線程同時執行,較大程度還原實際併發場景使用CountDownLatch模擬併發
public class TestThread extends Thread {
public static List<Long> idList = null;
public static void main(String[] args) throws InterruptedException {
idList = new ArrayList<Long>();
final CountDownLatch latch = new CountDownLatch(1);
for(int i = 0 ; i < 2 ;i ++ ){
Thread thread = new TestThread(latch,i);
thread.start();
}
Thread.sleep(5000); //延時2秒
System.out.println(idList);
System.out.println("去重前ID數量:"+idList.size());
idList = idList.stream().distinct().collect(Collectors.toList());
System.out.println("去重後ID數量:"+idList.size());
}
private CountDownLatch latch;
private int num;
public TestThread(CountDownLatch latch,int num) {
this.latch = latch;
this.num = num;
}
@Override
public void run() {
SnowflakeIdWorker3rd idWorker = new SnowflakeIdWorker3rd();
latch.countDown();
try {
latch.await();
for (int i = 0; i < 5; i++) {
long id = idWorker.nextId();
idList.add(id);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
這段代碼的意思是啓2個線程,每個線程生成5個ID,預期生成10個互不重複的ID,並把生成的ID去重前後的數量分別打印出來。
問題出現了,每次生成的ID都會有五個重複
再翻看一下大佬的代碼,問題找到了!代碼中synchronized關鍵字修飾了nextId()方法來確保不被重複調用,但是(敲黑板):修飾方法時鎖定的是調用該方法的對象,它並不能使調用該方法的多個對象在執行順序上互斥。所以當有多個線程都實例化了SnowflakeIdWorker3rd類並調用nextId方法,此時不能保證數據數據唯一。
對代碼進行簡單改造
改造方案是確保JVM中只有一個SnowflakeIdWorker3rd實例,爲SnowflakeIdWorker3rd建一個統一創建Id的方法createId(),並把nextId改爲private修飾,避免被其他人誤調。
改造完成再試試,結果符合預期。(數據缺、有值爲null是因爲list是線程不安全的)
生成唯一ID這種事一定要考慮多線程情況,不然出現了數據重複就打臉了!
完整代碼從github獲取:多線程創建唯一ID
文辭粗淺,不當之處請指教,如果覺得文章不錯,請關注或點贊 (-__-)謝謝