什么是时间轮
时间轮出自Netty中的HashedWheelTimer,是一个环形结构,可以用时钟来类比,钟面上有很多bucket,每一个bucket上可以存放多个任务,使用一个List保存该时刻到期的所有任务,同时一个指针随着时间流逝一格一格转动,并执行对应bucket上所有到期的任务。任务通过取模决定应该放入哪个bucket。和HashMap的原理类似,newTask对应put,使用List来解决 Hash 冲突。
时间轮怎么实现延迟队列
//时间轮对象中申明一个存放时间轮对象
private volatile static Map<Integer, List<Integer>> ringData = new ConcurrentHashMap<>();
- private ,是为了遵循开闭原则,防止外部直接操作时间轮
- volatile,是为了多线程可见性,防止工作内存未刷新主内存的值,导致出现数据不一致,关键字虽然可以保证部分有序性(具体请查阅相关资料),但实际代码操作中并没有使用。
- static,为了保证是多个实例使用一个类成员变量
- Map<Integer, List> 使用map结构是为了组装key-value(时间刻度-List<任务>)
最后需要使用并发map,是为了保证多线程操作不会出现线程问题
补充一点并发三要素
- 原子性:所有操作必须为原子的,天然原子操作就是赋值与四则运算,其中32位系统的除法运算不为原子操作
- 有序性:因为编译器会对代码进行优化,代码的顺序会出现变化,在单线程的情况下,对结果无影响。在多线程情况下如果出现对成员变量操作,且代码执行顺序对中间态有影响的
- 可见性:cpu操作的不是直接内存,而且有一个工作内存,也就是cpu的高速缓存(我们称之为工作内存),每个cpu的核都有一个工作内存
多核执行线程,涉及到cpu高速缓存问题。
cpu <——> 高速缓存(工作内存) <———> 主内存(物理内存)
1.启动线程,从物理内存读取一份到高速缓存中,此时stop是false,!stop为true,并循环执行
2.接下来执行,main线程从物理内存读取stop后更改成true,但此时stop并不保证写入物理内存的时效,此时另外一个线程的stop取的一直是false。因此会在这里导致循环不能退出,这里是一个典型的并发可见性问题。可以用volatile关键字修饰变量,保证修改后,强制写入物理内存,保证对另外一个线程的可见(这里是使用的cpu缓存一致算法实现的)
存放数据到时间轮
public void pushTimeRing(int ringSecond, int jobId){
// push async ring
List<Integer> ringItemData = ringData.get(ringSecond);
if (ringItemData == null) {
ringItemData = new ArrayList<Integer>();
ringData.put(ringSecond, ringItemData);
}
ringItemData.add(jobId);
logger.debug(">>>>>>>>>>> xxl-job, schedule push time-ring : " + ringSecond + " = " + Arrays.asList(ringItemData) );
}
存放刻度,与任务id,这里的刻度是秒,如果刻度不够用怎么办?最快的方法是加时间刻度,处理但是这么做,内存增加,利用率低,无用功做的比较多。最佳的办法是,加上一个圈数round,如下:
private volatile static Map<Integer, List<Map<Integer,Integer>>> ringData = new ConcurrentHashMap<>();
- List<Map<Integer,Integer>> 其中Map是存放当前刻度的对象,不管是第几圈的,Map的key是为了存放圈数round,value存放任务id等。
每次到当前刻度后遍历list,当round=0的时候执行任务后移除,如果任务不为0,则round=round-1;
刻度移动
在xxl-job中,处理如下:
try {
TimeUnit.MILLISECONDS.sleep(5000 - System.currentTimeMillis()%1000 );
} catch (InterruptedException e) {
if (!scheduleThreadToStop) {
logger.error(e.getMessage(), e);
}
}
- 5000 这个值是xxl-job的刻度,每次走5000,System.currentTimeMillis()%1000 取当前毫秒数,做差然后休眠,就是为了把时间轮对准每次5s的刻度
每次执行一次,然后就会校准刻度,中间是通过while循环实现的推动时间轮的
TimeUnit.MILLISECONDS.sleep((preReadSuc?1000:PRE_READ_MS) - System.currentTimeMillis()%1000);
这样就可以完成了延迟任务队列了