1

Introduction

A timer is a very common and effective tool in practical applications. Its principle is to sort the tasks to be executed according to the order of execution time, and then execute them at a specific time. JAVA provides a variety of Timer tools such as java.util.Timer and java.util.concurrent.ScheduledThreadPoolExecutor, but these tools still have some defects in execution efficiency, so netty provides HashedWheelTimer, an optimized Timer class.

Let's take a look at how Netty's Timer is different.

java.util.Timer

Timer was introduced in JAVA 1.3. All tasks are stored in TaskQueue inside it:

 private final TaskQueue queue = new TaskQueue();

The bottom layer of TaskQueue is an array of TimerTasks, which are used to store tasks to be executed.

 private TimerTask[] queue = new TimerTask[128];

It looks like TimerTask is just an array, but Timer makes this queue a balanced binary heap.

When adding a TimerTask, it will be inserted at the end of the Queue, and then the fixup method will be called to rebalance:

 void add(TimerTask task) {
        // Grow backing store if necessary
        if (size + 1 == queue.length)
            queue = Arrays.copyOf(queue, 2*queue.length);

        queue[++size] = task;
        fixUp(size);
    }

When the running task is removed from the heap, the fixDown method is called to rebalance:

 void removeMin() {
        queue[1] = queue[size];
        queue[size--] = null;  // Drop extra reference to prevent memory leak
        fixDown(1);
    }

The principle of fixup is to compare the current node with its parent node, and if it is smaller than the parent node, interact with the parent node, and then traverse the process:

 private void fixUp(int k) {
        while (k > 1) {
            int j = k >> 1;
            if (queue[j].nextExecutionTime <= queue[k].nextExecutionTime)
                break;
            TimerTask tmp = queue[j];  queue[j] = queue[k]; queue[k] = tmp;
            k = j;
        }
    }

The principle of fixDown is to compare the current node and its children, and if the current node is larger than the children, demote it:

 private void fixDown(int k) {
        int j;
        while ((j = k << 1) <= size && j > 0) {
            if (j < size &&
                queue[j].nextExecutionTime > queue[j+1].nextExecutionTime)
                j++; // j indexes smallest kid
            if (queue[k].nextExecutionTime <= queue[j].nextExecutionTime)
                break;
            TimerTask tmp = queue[j];  queue[j] = queue[k]; queue[k] = tmp;
            k = j;
        }
    }

The algorithm of the binary balanced heap is not described in detail here. You can find relevant articles by yourself.

java.util.concurrent.ScheduledThreadPoolExecutor

Although Timer is already very useful and thread-safe, for Timer, if you want to submit a task, you need to create a TimerTask class to encapsulate specific tasks, which is not very general.

So JDK 5.0 introduced a more general ScheduledThreadPoolExecutor, which is a thread pool that uses multiple threads to perform specific tasks. When the number of threads in the thread pool is equal to 1, ScheduledThreadPoolExecutor is equivalent to Timer.

A DelayedWorkQueue is used to save tasks in ScheduledThreadPoolExecutor.

DelayedWorkQueue is a heap-based data structure like DelayQueue and PriorityQueue.

Because the heap needs to continuously rebalance the siftUp and siftDown operations, its time complexity is O(log n).

The following is the implementation code of DelayedWorkQueue's shiftUp and siftDown:

 private void siftUp(int k, RunnableScheduledFuture<?> key) {
            while (k > 0) {
                int parent = (k - 1) >>> 1;
                RunnableScheduledFuture<?> e = queue[parent];
                if (key.compareTo(e) >= 0)
                    break;
                queue[k] = e;
                setIndex(e, k);
                k = parent;
            }
            queue[k] = key;
            setIndex(key, k);
        }

        private void siftDown(int k, RunnableScheduledFuture<?> key) {
            int half = size >>> 1;
            while (k < half) {
                int child = (k << 1) + 1;
                RunnableScheduledFuture<?> c = queue[child];
                int right = child + 1;
                if (right < size && c.compareTo(queue[right]) > 0)
                    c = queue[child = right];
                if (key.compareTo(c) <= 0)
                    break;
                queue[k] = c;
                setIndex(c, k);
                k = child;
            }
            queue[k] = key;
            setIndex(key, k);
        }

HashedWheelTimer

Because the bottom layer of Timer and ScheduledThreadPoolExecutor are based on heap structure. Although ScheduledThreadPoolExecutor improves Timer, the efficiency of the two is similar.

So is there a more efficient way? For example, is O(1) achievable?

We know that Hash can achieve efficient O(1) lookup, imagine if we have a clock with infinite scale, and then assign the tasks to be performed to these scales in the order of the interval time, every time the clock moves a scale, that is The tasks corresponding to this scale can be performed, as shown in the following figure:

This algorithm is called the Simple Timing Wheel algorithm.

But this algorithm is theoretical because it is impossible to assign corresponding ticks to all interval lengths. This will consume a lot of invalid memory space.

So we can make a compromise, and use the hash to process the length of the interval first. This reduces the base of the interval time, as shown in the following figure:

In this example, we choose 8 as the base, the interval is divided by 8, the remainder is used as the hash position, and the quotient is used as the value of the node.

Decrement the value of the node by one each time the poll is traversed. When the value of the node is 0, it means that the node can be fetched and executed.

This algorithm is called HashedWheelTimer.

netty provides an implementation of this algorithm:

 public class HashedWheelTimer implements Timer

HashedWheelTimer uses an array of HashedWheelBucket to store specific TimerTasks:

 private final HashedWheelBucket[] wheel;

First, let's take a look at how to create a wheel:

 private static HashedWheelBucket[] createWheel(int ticksPerWheel) {
        //ticksPerWheel may not be greater than 2^30
        checkInRange(ticksPerWheel, 1, 1073741824, "ticksPerWheel");

        ticksPerWheel = normalizeTicksPerWheel(ticksPerWheel);
        HashedWheelBucket[] wheel = new HashedWheelBucket[ticksPerWheel];
        for (int i = 0; i < wheel.length; i ++) {
            wheel[i] = new HashedWheelBucket();
        }
        return wheel;
    }

We can customize the size of ticks in the wheel, but ticksPerWheel cannot exceed 2^30.

Then adjust the value of ticksPerWheel to an integer multiple of 2.

Then create a HashedWheelBucket array of ticksPerWheel elements.

It should be noted here that although the overall wheel is a hash structure, each element in the wheel, that is, the HashedWheelBucket, is a chain structure.

Each element in HashedWheelBucket is a HashedWheelTimeout. There is a remainingRounds property in HashedWheelTimeout to record how long the Timeout element will remain in the Bucket.

 long remainingRounds;

Summarize

The HashedWheelTimer in netty can implement a more efficient Timer function, let's use it.

For more information, please refer to http://www.flydean.com/50-netty-hashed-wheel-timer/

The most popular interpretation, the most profound dry goods, the most concise tutorials, and many tricks you don't know are waiting for you to discover!

Welcome to pay attention to my official account: "Program those things", understand technology, understand you better!


flydean
890 声望433 粉丝

欢迎访问我的个人网站:www.flydean.com