Introduction

In my mind, there are two classic versions of JDK. The first is JDK8, which is used by most companies now. This version introduces Stream, lambda expressions and generics to make the writing of JAVA programs more smooth. A lot of redundant code is reduced.

Another version should be earlier, or the era of JAVA 1.X, we call it JDK1.5, this version introduces java.util.concurrent concurrent package, from now on, asynchronous programming can be used happily in JAVA.

Although the first JDK has developed to version 17, the changes in concurrency are not very big. Due to the requirement of JDK to maintain stability, the functions provided by concurrent packages cannot fully meet certain business scenarios. Therefore, packages that depend on JDK have developed their own concurrent packages.

Of course, netty is no exception. Let's take a look at the advantages of netty concurrent packages.

JDK asynchronous origin

How to create an asynchronous task in java, or start an asynchronous thread, everyone may have their own answer.

The first thing you might think of is to create a class that implements the Runnable interface, and then encapsulate it in Thread to run, as shown below:

new Thread(new(RunnableTask())).start()

It is unacceptable for the JDK gods to need a new Thread every time, so they came up with the idea of encapsulating thread calls, and this encapsulation class is called Executor.

Executor is an interface, first look at the definition of this interface:

public interface Executor {

    void execute(Runnable command);
}

The interface is very simple, that is, it defines an execute method to execute the incoming Runnable command.

So we can start the task asynchronously like this:

   Executor executor = anExecutor;
   executor.execute(new RunnableTask1());
   executor.execute(new RunnableTask2());

Seeing this, smart friends may ask, it seems wrong, Executor customizes the execute interface, it seems that it has nothing to do with asynchrony and multi-threading?

Don't worry, since Executor is an interface, we can have many implementations. For example, the following directly executes the Runnable and lets the Runnable execute in the current thread:

 class DirectExecutor implements Executor {
   public void execute(Runnable r) {
     r.run();
   }
 }

Another example is the following execution of Runnable in a new thread:

 class ThreadPerTaskExecutor implements Executor {
   public void execute(Runnable r) {
     new Thread(r).start();
   }
 }

Another example is the following sequence execution of storing multiple tasks in a Queue, executing one task and then executing the next task:

 class SerialExecutor implements Executor {
   final Queue<Runnable> tasks = new ArrayDeque<Runnable>();
   final Executor executor;
   Runnable active;

   SerialExecutor(Executor executor) {
     this.executor = executor;
   }

   public synchronized void execute(final Runnable r) {
     tasks.offer(new Runnable() {
       public void run() {
         try {
           r.run();
         } finally {
           scheduleNext();
         }
       }
     });
     if (active == null) {
       scheduleNext();
     }
   }

   protected synchronized void scheduleNext() {
     if ((active = tasks.poll()) != null) {
       executor.execute(active);
     }
   }
 }

These Executors are perfect. But they can only submit tasks, and they don't know anything after submitting tasks. This is unbearable for curious babies because we need to know the outcome of the execution, or control the execution of the task.

So there is ExecutorService. ExecutorService is also an interface, but it provides the shutdown method to stop accepting new tasks, and isShutdown to judge the shutdown status.

In addition, it also provides the submit method for calling tasks individually and the invokeAll and invokeAny methods for calling tasks in batches.

Now that there is the execute method, although the submit method basically performs the same operation as the execute method, there are slight differences in the method parameters and return values.

The first is the return value, the submit returns the Future, and the Future represents the result of the asynchronous calculation. It provides methods to check whether a calculation is complete, wait for it to complete, and retrieve the result of the calculation. Future provides the get method to obtain the calculation result. However, if the calculation result is not ready when the get method is called, blocking will occur.

The second is the parameter of submit. Generally speaking, only Callable will have a return value, so our commonly used calling method is as follows:

<T> Future<T> submit(Callable<T> task);

If we pass in Runnable, then although a Future is also returned, the returned value is null:

Future<?> submit(Runnable task);

What if I want to pass in Runnable and I want Future to have a return value?

The ancients told us that you can't have both! But it's 2021, and some things can change:

<T> Future<T> submit(Runnable task, T result);

We can pass in a result above, and return the result directly after the task in the Future is executed.

Since ExecutorService is so powerful, how to create ExecutorService?

The easiest way is to use new to create the corresponding instance. But this is not elegant enough, so JDK provides an Executors tool class, which provides a variety of static methods for creating different ExecutorServices, which are very easy to use.

Executor in netty

In order to be compatible with the concurrency framework of JDK, although there are Executors in netty, the Executors in netty are all derived from the concurrent packages of JDK.

Specifically, the Executor in netty is called EventExecutor, which inherits from EventExecutorGroup:

public interface EventExecutor extends EventExecutorGroup 

And EventExecutorGroup inherits from JDK's ScheduledExecutorService:

public interface EventExecutorGroup extends ScheduledExecutorService, Iterable<EventExecutor>

Why is it called a Group? This Group means that it contains a collection of EventExecutors. The EventExecutor in these combinations is traversed through the next method of Iterable.

This is why EventExecutorGroup also inherits the Iterable class.

Then the implementation of other specific Executors in netty is extended on the basis of EventExecutor. Thus, netty's own EventExecutor implementation is obtained.

The dilemma of Future and the realization of netty

So what's wrong with Future in JDK? Earlier we also mentioned that although the Future in the JDK saves the calculation result, we still need to call the get method to get it when we want to get it.

But if the current calculation result has not come out, the get method will cause the blocking of the current thread.

Fear not, this problem is solved in netty.

First look at the definition of Future in netty:

public interface Future<V> extends java.util.concurrent.Future<V> 

You can see that the Future in netty is the Future inherited from the JDK. Also added addListener and removeListener, as well as sync and await methods.

Let's talk about the sync and await methods first, both of which are waiting for the Future execution to end. The difference is that if during execution, if the future fails, an exception will be thrown. The await method does not.

Then if you don't want to call Future's get method synchronously to get the calculation result. You can add a listener to the Future.

In this way, when the Future execution ends, the method in the listener will be automatically notified to achieve the effect of asynchronous notification. The code used is as follows:

EventExecutorGroup group = new DefaultEventExecutorGroup(4); // 4 threads
Future<?> f = group.submit(new Runnable() { ... });
f.addListener(new FutureListener<?> {
  public void operationComplete(Future<?> f) {
    ..
  }
});

There is another question. Every time we submit a task, we need to create an EventExecutorGroup. Is there a way to submit a task without creating it?

some!

For those comrades who don't have time to create a new EventExecutorGroup, netty specially creates a global GlobalEventExecutor, which can be used directly:

GlobalEventExecutor.INSTANCE.execute(new Runnable() { ... });

GlobalEventExecutor is a single-threaded task executor that goes back every second to detect whether there are new tasks, and submits them to the executor for execution if there are any.

Summarize

netty provides very useful extensions to the JDK's concurrent packages. You can use it directly.

This article has been included in http://www.flydean.com/46-netty-future-executor/

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