之前有几个需要用到定时器超时的场景,比如线程池大小有限,如何让task不死等,但又不至于一旦队列满就直接reject或者让提交线程来做,但后来还是用让提交线程做事的方式来做,也就是并行暂时退化成了串行。
定时器有几个关键部分,1.定时扫描机制和设定,2.超时处理callback,3.超时前成功返回cancel.定时器的实现有很多种。下面的代码主要是团队使用的改造过的HashedWheelTimer,内部有很多细节,但不妨碍理解机制。
业务使用代码:
Timeout timeout = this.timer.newTimeout(new TimerTask() {
public void run(Timeout timeout) throws Exception {
// 如果连接没有推送元,则断开连接
if (!timeout.isCancelled() && conn.isConnected()) {
if (conn.getAttribute(CONN_META_DATA_ATTR) == null) {
log.error(LOGPREFX + "连接" + conn.getRemoteSocketAddress() + "没有推送元信息,主动关闭连接");
conn.close(false);
}
}
}
}, this.delay, TimeUnit.SECONDS);
// 已经添加了,取消最新的
if (conn.setAttributeIfAbsent(CONN_METADATA_TIMEOUT_ATTR, timeout) != null) {
timeout.cancel();
}
定时器实现代码
/**
* A {@link Timer} optimized for approximated I/O timeout scheduling.
*
* <h3>Tick Duration</h3>
*
* As described with 'approximated', this timer does not execute the scheduled
* {@link TimerTask} on time. {@link HashedWheelTimer}, on every tick, will
* check if there are any {@link TimerTask}s behind the schedule and execute
* them.
* <p>
* You can increase or decrease the accuracy of the execution timing by
* specifying smaller or larger tick duration in the constructor. In most
* network applications, I/O timeout does not need to be accurate. Therefore,
* the default tick duration is 100 milliseconds and you will not need to try
* different configurations in most cases.
*
* <h3>Ticks per Wheel (Wheel Size)</h3>
*
* {@link HashedWheelTimer} maintains a data structure called 'wheel'. To put
* simply, a wheel is a hash table of {@link TimerTask}s whose hash function is
* 'dead line of the task'. The default number of ticks per wheel (i.e. the size
* of the wheel) is 512. You could specify a larger value if you are going to
* schedule a lot of timeouts.
*
* <h3>Implementation Details</h3>
*
* {@link HashedWheelTimer} is based on <a
* href="http://cseweb.ucsd.edu/users/varghese/">George Varghese</a> and Tony
* Lauck's paper, <a
* href="http://cseweb.ucsd.edu/users/varghese/PAPERS/twheel.ps.Z">'Hashed and
* Hierarchical Timing Wheels: data structures to efficiently implement a timer
* facility'</a>. More comprehensive slides are located <a
* href="http://www.cse.wustl.edu/~cdgill/courses/cs6874/TimingWheels.ppt"
* >here</a>.
*
* @author <a href="http://www.jboss.org/netty/">The Netty Project</a>
* @author <a href="http://gleamynode.net/">Trustin Lee</a>
*/
public class HashedWheelTimer implements Timer {
static final Log logger = LogFactory.getLog(HashedWheelTimer.class);
private static final AtomicInteger id = new AtomicInteger();
// I'd say 64 active timer threads are obvious misuse.
private static final int MISUSE_WARNING_THRESHOLD = 64;
private static final AtomicInteger activeInstances = new AtomicInteger();
private static final AtomicBoolean loggedMisuseWarning = new AtomicBoolean();
private final Worker worker = new Worker();
final Thread workerThread;
final AtomicBoolean shutdown = new AtomicBoolean();
private final long roundDuration;
final long tickDuration;
final Set<HashedWheelTimeout>[] wheel;
final ReusableIterator<HashedWheelTimeout>[] iterators;
final int mask;
final ReadWriteLock lock = new ReentrantReadWriteLock();
private volatile int wheelCursor;
private final AtomicInteger size = new AtomicInteger(0);
final int maxTimerCapacity;
/**
* Creates a new timer with the default thread factory (
* {@link Executors#defaultThreadFactory()}), default tick duration, and
* default number of ticks per wheel.
*/
public HashedWheelTimer() {
this(Executors.defaultThreadFactory());
}
/**
* Creates a new timer with the default thread factory (
* {@link Executors#defaultThreadFactory()}) and default number of ticks per
* wheel.
*
* @param tickDuration
* the duration between tick
* @param unit
* the time unit of the {@code tickDuration}
*/
public HashedWheelTimer(long tickDuration, TimeUnit unit) {
this(Executors.defaultThreadFactory(), tickDuration, unit);
}
/**
* Creates a new timer with the default thread factory (
* {@link Executors#defaultThreadFactory()}).
*
* @param tickDuration
* the duration between tick
* @param unit
* the time unit of the {@code tickDuration}
* @param ticksPerWheel
* the size of the wheel
*/
public HashedWheelTimer(long tickDuration, TimeUnit unit, int ticksPerWheel, int maxTimerCapacity) {
this(Executors.defaultThreadFactory(), tickDuration, unit, ticksPerWheel, maxTimerCapacity);
}
/**
* Creates a new timer with the default tick duration and default number of
* ticks per wheel.
*
* @param threadFactory
* a {@link ThreadFactory} that creates a background
* {@link Thread} which is dedicated to {@link TimerTask}
* execution.
*/
public HashedWheelTimer(ThreadFactory threadFactory) {
this(threadFactory, 100, TimeUnit.MILLISECONDS);
}
/**
* 返回当前timer个数
*
* @return
*/
public int size() {
return this.size.get();
}
/**
* Creates a new timer with the default number of ticks per wheel.
*
* @param threadFactory
* a {@link ThreadFactory} that creates a background
* {@link Thread} which is dedicated to {@link TimerTask}
* execution.
* @param tickDuration
* the duration between tick
* @param unit
* the time unit of the {@code tickDuration}
*/
public HashedWheelTimer(ThreadFactory threadFactory, long tickDuration, TimeUnit unit) {
this(threadFactory, tickDuration, unit, 512, 50000);
}
/**
* Creates a new timer.
*
* @param threadFactory
* a {@link ThreadFactory} that creates a background
* {@link Thread} which is dedicated to {@link TimerTask}
* execution.
* @param tickDuration
* the duration between tick
* @param unit
* the time unit of the {@code tickDuration}
* @param ticksPerWheel
* @param maxTimerCapacity
* the size of the wheel
*/
public HashedWheelTimer(ThreadFactory threadFactory, long tickDuration, TimeUnit unit, int ticksPerWheel,
int maxTimerCapacity) {
if (threadFactory == null) {
throw new NullPointerException("threadFactory");
}
if (unit == null) {
throw new NullPointerException("unit");
}
if (tickDuration <= 0) {
throw new IllegalArgumentException("tickDuration must be greater than 0: " + tickDuration);
}
if (ticksPerWheel <= 0) {
throw new IllegalArgumentException("ticksPerWheel must be greater than 0: " + ticksPerWheel);
}
if (maxTimerCapacity <= 0) {
throw new IllegalArgumentException("maxTimerCapacity must be greater than 0: " + maxTimerCapacity);
}
// Normalize ticksPerWheel to power of two and initialize the wheel.
this.wheel = createWheel(ticksPerWheel);
this.iterators = createIterators(this.wheel);
this.maxTimerCapacity = maxTimerCapacity;
this.mask = this.wheel.length - 1;
// Convert tickDuration to milliseconds.
this.tickDuration = tickDuration = unit.toMillis(tickDuration);
// Prevent overflow.
if (tickDuration == Long.MAX_VALUE || tickDuration >= Long.MAX_VALUE / this.wheel.length) {
throw new IllegalArgumentException("tickDuration is too long: " + tickDuration + ' ' + unit);
}
this.roundDuration = tickDuration * this.wheel.length;
this.workerThread =
threadFactory.newThread(new ThreadRenamingRunnable(this.worker, "Hashed wheel timer #"
+ id.incrementAndGet()));
// Misuse check
int activeInstances = HashedWheelTimer.activeInstances.incrementAndGet();
if (activeInstances >= MISUSE_WARNING_THRESHOLD && loggedMisuseWarning.compareAndSet(false, true)) {
logger.debug("There are too many active " + HashedWheelTimer.class.getSimpleName() + " instances ("
+ activeInstances + ") - you should share the small number "
+ "of instances to avoid excessive resource consumption.");
}
}
@SuppressWarnings("unchecked")
private static Set<HashedWheelTimeout>[] createWheel(int ticksPerWheel) {
if (ticksPerWheel <= 0) {
throw new IllegalArgumentException("ticksPerWheel must be greater than 0: " + ticksPerWheel);
}
if (ticksPerWheel > 1073741824) {
throw new IllegalArgumentException("ticksPerWheel may not be greater than 2^30: " + ticksPerWheel);
}
ticksPerWheel = normalizeTicksPerWheel(ticksPerWheel);
Set<HashedWheelTimeout>[] wheel = new Set[ticksPerWheel];
for (int i = 0; i < wheel.length; i++) {
wheel[i] =
new MapBackedSet<HashedWheelTimeout>(new ConcurrentIdentityHashMap<HashedWheelTimeout, Boolean>(16,
0.95f, 4));
}
return wheel;
}
@SuppressWarnings("unchecked")
private static ReusableIterator<HashedWheelTimeout>[] createIterators(Set<HashedWheelTimeout>[] wheel) {
ReusableIterator<HashedWheelTimeout>[] iterators = new ReusableIterator[wheel.length];
for (int i = 0; i < wheel.length; i++) {
iterators[i] = (ReusableIterator<HashedWheelTimeout>) wheel[i].iterator();
}
return iterators;
}
private static int normalizeTicksPerWheel(int ticksPerWheel) {
int normalizedTicksPerWheel = 1;
while (normalizedTicksPerWheel < ticksPerWheel) {
normalizedTicksPerWheel <<= 1;
}
return normalizedTicksPerWheel;
}
/**
* Starts the background thread explicitly. The background thread will start
* automatically on demand even if you did not call this method.
*
* @throws IllegalStateException
* if this timer has been {@linkplain #stop() stopped} already
*/
public synchronized void start() {
if (this.shutdown.get()) {
throw new IllegalStateException("cannot be started once stopped");
}
if (!this.workerThread.isAlive()) {
this.workerThread.start();
}
}
public synchronized Set<Timeout> stop() {
if (!this.shutdown.compareAndSet(false, true)) {
return Collections.emptySet();
}
boolean interrupted = false;
while (this.workerThread.isAlive()) {
this.workerThread.interrupt();
try {
this.workerThread.join(100);
}
catch (InterruptedException e) {
interrupted = true;
}
}
if (interrupted) {
Thread.currentThread().interrupt();
}
activeInstances.decrementAndGet();
Set<Timeout> unprocessedTimeouts = new HashSet<Timeout>();
for (Set<HashedWheelTimeout> bucket : this.wheel) {
unprocessedTimeouts.addAll(bucket);
bucket.clear();
}
return Collections.unmodifiableSet(unprocessedTimeouts);
}
public Timeout newTimeout(TimerTask task, long delay, TimeUnit unit) {
final long currentTime = System.currentTimeMillis();
if (task == null) {
throw new NullPointerException("task");
}
if (unit == null) {
throw new NullPointerException("unit");
}
delay = unit.toMillis(delay);
if (delay < this.tickDuration) {
delay = this.tickDuration;
}
if (!this.workerThread.isAlive()) {
this.start();
}
if (this.size.get() >= this.maxTimerCapacity) {
throw new RejectedExecutionException("Timer size " + this.size + " is great than maxTimerCapacity "
+ this.maxTimerCapacity);
}
// Prepare the required parameters to create the timeout object.
HashedWheelTimeout timeout;
final long lastRoundDelay = delay % this.roundDuration;
final long lastTickDelay = delay % this.tickDuration;
final long relativeIndex = lastRoundDelay / this.tickDuration + (lastTickDelay != 0 ? 1 : 0);
final long deadline = currentTime + delay;
final long remainingRounds = delay / this.roundDuration - (delay % this.roundDuration == 0 ? 1 : 0);
// Add the timeout to the wheel.
this.lock.readLock().lock();
try {
timeout =
new HashedWheelTimeout(task, deadline, (int) (this.wheelCursor + relativeIndex & this.mask),
remainingRounds);
this.wheel[timeout.stopIndex].add(timeout);
}
finally {
this.lock.readLock().unlock();
}
this.size.incrementAndGet();
return timeout;
}
private final class Worker implements Runnable {
private long startTime;
private long tick;
Worker() {
super();
}
public void run() {
List<HashedWheelTimeout> expiredTimeouts = new ArrayList<HashedWheelTimeout>();
this.startTime = System.currentTimeMillis();
this.tick = 1;
while (!HashedWheelTimer.this.shutdown.get()) {
this.waitForNextTick();
this.fetchExpiredTimeouts(expiredTimeouts);
this.notifyExpiredTimeouts(expiredTimeouts);
}
}
private void fetchExpiredTimeouts(List<HashedWheelTimeout> expiredTimeouts) {
// Find the expired timeouts and decrease the round counter
// if necessary. Note that we don't send the notification
// immediately to make sure the listeners are called without
// an exclusive lock.
HashedWheelTimer.this.lock.writeLock().lock();
try {
int oldBucketHead = HashedWheelTimer.this.wheelCursor;
int newBucketHead = oldBucketHead + 1 & HashedWheelTimer.this.mask;
HashedWheelTimer.this.wheelCursor = newBucketHead;
ReusableIterator<HashedWheelTimeout> i = HashedWheelTimer.this.iterators[oldBucketHead];
this.fetchExpiredTimeouts(expiredTimeouts, i);
}
finally {
HashedWheelTimer.this.lock.writeLock().unlock();
}
}
private void fetchExpiredTimeouts(List<HashedWheelTimeout> expiredTimeouts,
ReusableIterator<HashedWheelTimeout> i) {
long currentDeadline = System.currentTimeMillis() + HashedWheelTimer.this.tickDuration;
i.rewind();
while (i.hasNext()) {
HashedWheelTimeout timeout = i.next();
if (timeout.remainingRounds <= 0) {
if (timeout.deadline < currentDeadline) {
i.remove();
expiredTimeouts.add(timeout);
}
else {
// A rare case where a timeout is put for the next
// round: just wait for the next round.
}
}
else {
timeout.remainingRounds--;
}
}
}
private void notifyExpiredTimeouts(List<HashedWheelTimeout> expiredTimeouts) {
// Notify the expired timeouts.
for (int i = expiredTimeouts.size() - 1; i >= 0; i--) {
expiredTimeouts.get(i).expire();
HashedWheelTimer.this.size.decrementAndGet();
}
// Clean up the temporary list.
expiredTimeouts.clear();
}
private void waitForNextTick() {
for (;;) {
final long currentTime = System.currentTimeMillis();
final long sleepTime = HashedWheelTimer.this.tickDuration * this.tick - (currentTime - this.startTime);
if (sleepTime <= 0) {
break;
}
try {
Thread.sleep(sleepTime);
}
catch (InterruptedException e) {
if (HashedWheelTimer.this.shutdown.get()) {
return;
}
}
}
// Reset the tick if overflow is expected.
if (HashedWheelTimer.this.tickDuration * this.tick > Long.MAX_VALUE - HashedWheelTimer.this.tickDuration) {
this.startTime = System.currentTimeMillis();
this.tick = 1;
}
else {
// Increase the tick if overflow is not likely to happen.
this.tick++;
}
}
}
private final class HashedWheelTimeout implements Timeout {
private final TimerTask task;
final int stopIndex;
final long deadline;
volatile long remainingRounds;
private volatile boolean cancelled;
HashedWheelTimeout(TimerTask task, long deadline, int stopIndex, long remainingRounds) {
this.task = task;
this.deadline = deadline;
this.stopIndex = stopIndex;
this.remainingRounds = remainingRounds;
}
public Timer getTimer() {
return HashedWheelTimer.this;
}
public TimerTask getTask() {
return this.task;
}
public void cancel() {
if (this.isExpired()) {
return;
}
this.cancelled = true;
// Might be called more than once, but doesn't matter.
if (HashedWheelTimer.this.wheel[this.stopIndex].remove(this)) {
HashedWheelTimer.this.size.decrementAndGet();
}
}
public boolean isCancelled() {
return this.cancelled;
}
public boolean isExpired() {
return this.cancelled || System.currentTimeMillis() > this.deadline;
}
public void expire() {
if (this.cancelled) {
return;
}
try {
this.task.run(this);
}
catch (Throwable t) {
logger.warn("An exception was thrown by " + TimerTask.class.getSimpleName() + ".", t);
}
}
@Override
public String toString() {
long currentTime = System.currentTimeMillis();
long remaining = this.deadline - currentTime;
StringBuilder buf = new StringBuilder(192);
buf.append(this.getClass().getSimpleName());
buf.append('(');
buf.append("deadline: ");
if (remaining > 0) {
buf.append(remaining);
buf.append(" ms later, ");
}
else if (remaining < 0) {
buf.append(-remaining);
buf.append(" ms ago, ");
}
else {
buf.append("now, ");
}
if (this.isCancelled()) {
buf.append(", cancelled");
}
return buf.append(')').toString();
}
}
}