完整代码从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
文辞粗浅,不当之处请指教,如果觉得文章不错,请关注或点赞 (-__-)谢谢