自顶向下深入分析Netty(四)--EventLoop-1
来源:互联网 发布:女士西装品牌知乎 编辑:程序博客网 时间:2024/06/06 07:02
我们再次回顾这幅图,通过先前的讲解,现在是不是亲切很多了。图中绿色的acceptor应该是你最熟悉的部分,之前我们在ServerBootstrap中进行了详细分析。我们知道了mainReactor是一个线程池,处理Accept事件负责接受客户端的连接;subReactor也是一个线程池,处理Read(读取客户端通道上的数据)、Write(将数据写入到客户端通道上)等事件。在这一节中,我们将深入分析这两个线程池的实现,不断完善其中的细节。我们首先从类图开始。
4.1 类图
看到这幅类图,如果你的第一印象是气势恢宏,那么恭喜你,你已经成功了一半。但不难预料的是,大多数人和我的感受是一样的:这么多类,一定很累。好在这只是第一印象,我们仔细观察,便会发现其中明显的脉络,两条线索(这里使用自下而上):NioEventLoop以及NioEventLoopGroup即线程和线程池。忽略其中大量的接口,剩余这样的两条线:
NioEventLoop --> SingleThreadEventLoop --> SingleThreadEventExecutor -->AbstractScheduledEventExecutor --> AbstractScheduledEventExecutor --> AbstractEventExecutor --> AbstractExecutorServiceNioEventLoopGroup --> MultithreadEventLoopGroup --> MultithreadEventExecutorGroup --> AbstractEventExecutorGroup
下面我们正式开始分析,依旧使用自顶向下的方法,从类图顶部向下、从线程池到线程分析。
4.2 EventExecutorGroup
EventExecutorGroup在类图中处于承上启下的位置,其上是Java原生的接口和类,其下是Netty新建的接口和类,由于它处于如此重要的位置,我们详细分析其中的方法。
4.2.1 Executor
首先看其继承自Executor的方法:
// Executes the given command at some time in the future void execute(Runnable command);
只有一个简单的execute()方法,但这个方法奠定了java并发的基础,提供了异步执行任务的。
4.2.2 ExecutorService
ExecutorService的关键方法如下(其中的invoke*方法并非关键,不再列出):
void shutdown(); List<Runnable> shutdownNow(); boolean isShutdown(); boolean isTerminated(); boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException; <T> Future<T> submit(Callable<T> task); <T> Future<T> submit(Runnable task, T result); Future<?> submit(Runnable task);
这些方法我们能从命名中便能知道方法的作用。我们主要看submit()方法,该方法是execute()方法的扩展,相较于execute不关心执行结果,submit返回一个异步执行结果Future。这无疑是很大的进步,但这里的Future不提供回调操作,显得很鸡肋,所以Netty将Java原生的java.util.concurrent.Future扩展为io.netty.util.concurrent.Future,我们将在之后进行介绍。
4.2.3 ScheduledExecutorService
从名字可以看出,该接口提供了一系列调度方法:
ScheduledFuture<?> schedule(Runnable command,long delay, TimeUnit unit); <V> ScheduledFuture<V> schedule(Callable<V> callable,long delay, TimeUnit unit); ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long initialDelay, long period,TimeUnit unit); ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,long initialDelay, long delay,TimeUnit unit);
schedule()方法调度任务使任务在延迟一段时间后执行。scheduleAtFixedRate延迟一段时间后以固定频率执行任务,scheduleWithFixedDelay延迟一段时间后以固定延时执行任务。是不是有点头晕?那就对了,这里有一个例子专门治头晕。专家建议程序员应该每小时工作50分钟,休息10分钟,类似这样:
13:00 - 13:10 休息 13:10 - 14:00 写代码 14:00 - 14:10 休息 14:10 - 15:00 写代码
实现这样的调度我们可以使用(假设现在时间为13:00):
executor.scheduleAtFixedRate(new RestRunnable(), 0 , 60, TimeUnit.MINUTES); executor.scheduleWithFixedDelay(new RestRunnable(), 0 , 50, TimeUnit.MINUTES);
4.2.4 EventExecutorGroup
boolean isShuttingDown(); Future<?> shutdownGracefully(); Future<?> shutdownGracefully(long quietPeriod, long timeout, TimeUnit unit); Future<?> terminationFuture(); EventExecutor next();
EventExecutorGroup扩展的方法有5个,前四个可以从命名中推断出功能。shutdownGracefully()我们已经在Bootstrap一节中使用过,优雅关闭线程池;terminationFuture()返回线程池终止时的异步结果。重点关注next()方法,该方法的功能是从线程池中选择一个线程。EventExecutorGroup还覆盖了一些方法,我们不再列出,如果你感兴趣可以去源码里面查看,需要注意的是,覆盖的方法大部分是将Java原生的java.util.concurrent.Future返回值覆盖为io.netty.util.concurrent.Future。
4.3 线程池
4.3.1 AbstractEventExecutorGroup
AbstractEventExecutorGroup实现了EventExecutorGroup接口的大部分方法,实现都长的和下面的差不多:
@Override public void execute(Runnable command) { next().execute(command); }
从这段代码可以看出这个线程池和程序员有一个相同点:懒。当线程池执行一个任务或命令时,步骤是这样的:(1).找一个线程。(2).交给线程执行。
4.3.2 MultithreadEventExecutorGroup
MultithreadEventExecutorGroup实现了线程的创建和线程的选择,其中的字段为:
// 线程池,数组形式可知为固定线程池 private final EventExecutor[] children; // 线程索引,用于线程选择 private final AtomicInteger childIndex = new AtomicInteger(); // 终止的线程个数 private final AtomicInteger terminatedChildren = new AtomicInteger(); // 线程池终止时的异步结果 private final Promise<?> terminationFuture = new DefaultPromise(GlobalEventExecutor.INSTANCE); // 线程选择器 private final EventExecutorChooser chooser;
MultithreadEventExecutorGroup的构造方法很长,我们将选出其中的关键部分分析,故不列出整体代码。如果你是处女座,这里有一个链接MultithreadEventExecutorGroup。
我们先看构造方法签名:
protected MultithreadEventExecutorGroup(int nThreads, ThreadFactory threadFactory, Object... args)
其中的nThreads表示线程池的固定线程数。
MultithreadEventExecutorGroup初始化的步骤是:
(1).设置线程工厂
(2).设置线程选择器
(3).实例化线程
(4).设置线程终止异步结果
首先我们看设备线程工厂的代码:
if (threadFactory == null) { threadFactory = newDefaultThreadFactory(); } protected ThreadFactory newDefaultThreadFactory(),() { return new DefaultThreadFactory(getClass()); }
如果构造参数threadFactory为空则使用默认线程池,创建默认线程池使用newDefaultThreadFactory(),这是一个protected方法,可以在子类中覆盖实现。
接着我们看设置线程选择器的代码:
if (isPowerOfTwo(children.length)) { chooser = new PowerOfTwoEventExecutorChooser(); } else { chooser = new GenericEventExecutorChooser(); }
如果线程数是2的幂次方使用2的幂次方选择器,否则使用通用选择器。下次如果有面试官问你怎么判断一个整数是2的幂次方,请甩给他这一行代码:
private static boolean isPowerOfTwo(int val) { return (val & -val) == val; }
Netty实现了两个线程选择器,虽然代码不一致,功能都是一样的:每次选择索引为上一次所选线程索引+1的线程。如果你没看明白代码的含义,没关系,再看一遍。
private interface EventExecutorChooser { EventExecutor next(); } private final class PowerOfTwoEventExecutorChooser implements EventExecutorChooser { @Override public EventExecutor next() { return children[childIndex.getAndIncrement() & children.length - 1]; } } private final class GenericEventExecutorChooser implements EventExecutorChooser { @Override public EventExecutor next() { return children[Math.abs(childIndex.getAndIncrement() % children.length)]; } }
最佳实践:线程池数量使用2的幂次方,这样线程池选择线程时使用位操作,能使性能最高。
下面我们接着分析实例化线程的步骤:
for (int i = 0; i < nThreads; i ++) { boolean success = false; try { // 使用模板方法newChild实例化一个线程 children[i] = newChild(threadFactory, args); success = true; } catch (Exception e) { throw new IllegalStateException("failed to create a child event loop", e); } finally { if (!success) { // 如果不成功,所有已经实例化的线程优雅关闭 for (int j = 0; j < i; j ++) { children[j].shutdownGracefully(); } // 确保已经实例化的线程终止 for (int j = 0; j < i; j ++) { EventExecutor e = children[j]; try { while (!e.isTerminated()) { e.awaitTermination(Integer.MAX_VALUE, TimeUnit.SECONDS); } } catch (InterruptedException interrupted) { Thread.currentThread().interrupt(); break; } } } } }
实现的过程一句话描述就是:使用newChild()依次实例化线程,如果出错,关闭所有已经实例化的线程。也许你对finally中的代码有疑问,这是因为不清楚shutdownGracefully()的含义。你需要提前明白这样的事实:shutdownGracefully()只是通知线程池该关闭,但什么时候关闭由线程池决定,所以需要使用e.isTerminated()来判断线程池是否真正关闭。
实例化线程池正常完成后,Netty使用下面的代码设置异步终止结果:
final FutureListener<Object> terminationListener = new FutureListener<Object>() { @Override public void operationComplete(Future<Object> future) throws Exception { // 线程池中的线程每终止一个增加记录数,直到全部终止设置线程池异步终止结果为成功 if (terminatedChildren.incrementAndGet() == children.length) { terminationFuture.setSuccess(null); } } }; for (EventExecutor e: children) { e.terminationFuture().addListener(terminationListener); }
分析完MultithreadEventExecutorGroup的构造方法,我们继续分析普通方法。它的普通方法基本与下面的isTerminated()类似:
@Override public boolean isTerminated() { for (EventExecutor l: children) { if (!l.isTerminated()) { return false; } } return true; }
总结起来就是:线程池的状态由其中的各个线程决定。明白了这点,我们使用类比的方法可以推知其他方法的实现,故不再具体分析。
4.3.3 MultithreadEventLoopGroup
MultithreadEventLoopGroup实现了EventLoopGroup接口的方法,EventLoopGroup接口作为Netty并发的关键接口,我们看其中扩展的方法:
// 将通道channel注册到EventLoopGroup中的一个线程上 ChannelFuture register(Channel channel); // 返回的ChannelFuture为传入的ChannelPromise ChannelFuture register(Channel channel, ChannelPromise promise); // 覆盖父类接口的方法,返回EventLoop @Override EventLoop next();
这些方法在MultithreadEventLoopGroup的具体实现很简单。register()方法选择一个线程,该线程负责具体的register()实现。next()方法使用父类实现,即使用上一节所述的选择器选择一个线程。代码如下:
@Override public ChannelFuture register(Channel channel) { return next().register(channel); } @Override public EventLoop next() { return (EventLoop) super.next(); }
分析完这些代码,我们关注一下线程数的默认设置。
DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt( "io.netty.eventLoopThreads", Runtime.getRuntime().availableProcessors() * 2));
默认情况,线程数最小为1,如果配置了系统参数io.netty.eventLoopThreads
,设置为该系统参数值,否则设置为核心数的2倍。
4.3.4 NioEventLoopGroup
NioEventLoopGroup的主要代码实现是模板方法newChild(),用来创建线程池中的单个线程,代码如下:
@Override protected EventExecutor newChild(ThreadFactory threadFactory, Object... args) throws Exception { return new NioEventLoop(this, threadFactory, (SelectorProvider) args[0], ((SelectStrategyFactory) args[1]).newSelectStrategy(), (RejectedExecutionHandler) args[2]); }
关于代码中的参数含义,我们放在NioEventLoop中分析。此外NioEventLoopGroup还提供了setIoRatio()和rebuildSelectors()两个方法,一个用来设置I/O任务和非I/O任务的执行时间比,一个用来重建线程中的selector来规避JDK的epoll 100% CPU Bug。其实现也是依次设置各线程的状态,故不再列出。
- 自顶向下深入分析Netty(四)--EventLoop-1
- 自顶向下深入分析Netty(四)--EventLoop-2
- 自顶向下深入分析Netty(四)--优雅退出机制
- 自顶向下深入分析Netty(一)--预备知识
- 自顶向下深入分析Netty(二)--线程模型
- 自顶向下深入分析Netty(三)--Bootstrap
- 自顶向下深入分析Netty(五)--Future
- 自顶向下深入分析Netty(八)--ChannelHandler
- 自顶向下深入分析Netty(八)--CodecHandler
- 自顶向下深入分析Netty(八)-- LengthFieldBasedFrameDecoder
- 自顶向下深入分析Netty(九)--ByteBuf
- 自顶向下深入分析Netty(九)--引用计数
- 自顶向下深入分析Netty(三)--Bootstrap源码分析
- 自顶向下深入分析Netty(九)--ByteBuf源码分析
- 自顶向下深入分析Netty(六)--Channel总述
- 自顶向下深入分析Netty(六)--Channel源码实现
- 自顶向下深入分析Netty(七)--ChannelPipeline源码实现
- 自顶向下深入分析Netty(七)--ChannelPipeline和ChannelHandler总述
- jquery如何改变元素的定位函数格式
- LogUtils
- 详解Java的Spring框架中bean的注入集合
- tensorflow预训练参数解析
- centos 的中文字体安装
- 自顶向下深入分析Netty(四)--EventLoop-1
- css3 box-shadow 属性的使用
- Unity3D里实现物体移动到目标点,并且摄像机跟着移动的简单方案
- 以史为鉴,论创业公司如何走向成熟
- android 原子操作
- JAVA中的Random()函数
- Linux相关文章索引(4)
- SSH项目中遇到的懒加载的问题
- java正则java.lang.IllegalStateException: No match found关于find和group方法使用问题