说明:netty源码系列是基于4.1.25版本的netty源码的

Netty作为一个Java生态中的网络组件有着举足轻重的位置,各种开源中间件都使用Netty进行网络通信,比如Dubbo、RocketMQ。可以说Netty是对Java NIO的封装,比如ByteBuf、channel等的封装让网络编程更简单。

在介绍Netty服务器启动之前需要简单了解两件事:

  1. reactor线程模型
  2. linux中的IO多路复用

reactor线程模型

关于reactor线程模型请参考这篇文章,通过不同的配置Netty可以实现对应的三种reactor线程模型

  • reactor单线程模型
  • reactor多线程模型
  • reactor主从多线程模型
 // reactor单线程模型,accept、connect、read、write都在一个线程中执行
EventLoopGroup group = new NioEventLoopGroup(1);
bootStrap.group(group); // reactor多线程,accept在bossGroup中的一个线程执行,IO操作在workerGroup中的线程执行
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
bootStrap.group(bossGroup , workerGroup); // reactor主从多线程,用来accept连接的是在一个线程池中执行,这个时候需要bind多个port,因为Netty一个bind的port会启动一个线程来accept
EventLoopGroup bossGroup = new NioEventLoopGroup(2);
EventLoopGroup workerGroup = new NioEventLoopGroup();
bootStrap.group(bossGroup , workerGroup);

注意:本文后面的介绍如无特别说明都是基于reactor多线程模型

linux中的IO多路复用

linux中的网络编程模型也是在不断演变的,下面是依次演变的顺序(具体可参考《UNIX网络编程卷1:套接字联网API》第三版的第4、6章)

accept

阻塞等待连接,接收到新的连接后新起线程来处理接收到的连接,然后在新的线程中阻塞等待新的数据到来

select

根据入参的不同有三种情况

  1. 永远等下去,直到监听的描述符有任意的IO事件才返回
  2. 等待一段固定时间,如果时间到之前有IO事件则提前返回,否则等待超时后返回
  3. 不等待,检查描述符后立即返回,称为轮询

select会返回就绪的文件描述符的个数,需要轮询所有socket,判断每个socket的状态来确定是否有事件、是什么事件

poll

相比较于selectpoll是阻塞等待的,只有有读写事件的时候才会返回,返回的是有读写事件的socket个数,并且将对应的socket的事件置位,自己从所有socket中找到具体的socket

epoll

相比较于poll,epoll可以将只有确实有IO事件的描述符返回,大并发下只有少量活跃连接的情况下使用

较poll的优势

  1. 不用开发者重新准备文件描述符集合(较poll入参简单)
  2. 无需遍历所有监听的描述符,只要遍历哪些被内核IO事件异步唤醒而加入ready队列的描述符集合

Java NIO在linux的实现就是基于epoll的。epoll的编程模型:

  1. 创建socket,socket方法
  2. 绑定服务器ip,port,bind方法
  3. 监听绑定了ip:port的文件描述符,listen方法
  4. 创建epoll句柄(文件描述符),配置最大监听的文件描述符个数,epoll_create方法
  5. 配置epoll监听的文件描述符的事件:注册、修改、删除某个文件描述符对应的事件
  6. 监听所有已配置的描述符,epoll_wait
  7. 有新的事件的时候遍历返回的描述符,处理对应的事件
  8. 如果是来自客户端的连接,则将accept到的文件描述符注册到epoll中
  9. 如果是读写事件则分别处理

注意:Netty封装的Java NIO是跨平台的,后面还是以linux平台为例来介绍

接下来言归正传,来看看Netty的服务器启动过程做了什么事情。Netty作为一个网络框架,和普通网络编程做的事情基本上一样,对应于上面epoll的编程模型,Netty的启动过程为

  1. 初始化线程池,初始化selector
  2. 初始化NioServerSocketChannel
  3. 绑定服务器ip:port
  4. 将NioServerSocketChannel注册到selector中
  5. 配置NioServerSocketChannel监听的事件
  6. 使用selector.select等待新的IO事件
  7. 如果是来自客户端的连接则将NioSocketChannel注册到selector上(如果是新的线程则是新的selector)
  8. 如果是普通IO事件则在worker线程中处理

线程池初始化

在介绍NioEventLoopGroup之前先看下NioEventLoop

可以看到NioEventLoop继承自SingleThreadEventExecutor,是一个单线程的executor,在线程中死循环监听IO事件。主要方法有

// 初始化selector
io.netty.channel.nio.NioEventLoop#openSelector
// 将channel注册到selector
io.netty.channel.nio.NioEventLoop#register
// 监听selector上的事件
io.netty.channel.nio.NioEventLoop#select

一个NioEventLoop会初始化一个selector,处理selector上注册的channel。

NioEventLoopGroup从名字上就可以看出来是由多个NioEventLoop组成,类关系图如下

NioEventLoopGroup的重要属性为:

// 包含的EventExecutor数组
private final EventExecutor[] children;
// 选择哪一个EventExecutor执行task的选择器,不同的选择器有不同的策略
private final EventExecutorChooserFactory.EventExecutorChooser chooser;

重要方法有:

// 选择下一个执行任务的线程
io.netty.util.concurrent.MultithreadEventExecutorGroup#next
// 创建EventLoop
io.netty.channel.nio.NioEventLoopGroup#newChild
// 在线程池中执行注册channel的任务
io.netty.channel.MultithreadEventLoopGroup#register(io.netty.channel.Channel)
// 创建默认的threadFactory
io.netty.channel.MultithreadEventLoopGroup#newDefaultThreadFactory

线程池初始化的代码为

EventLoopGroup workerGroup = new NioEventLoopGroup();

如果使用无参的构造方法的话,最后会执行下面这个构造方法,这里面做要做了以下几件事

  • 如果executor没有初始化,使用默认的executor初始化
  • 初始化线程池中每个EventLoop
  • 如果其中一个初始化过程中抛出异常,关闭所有的NioEventLoop
protected MultithreadEventExecutorGroup(int nThreads, Executor executor,
EventExecutorChooserFactory chooserFactory, Object... args) {
if (nThreads <= 0) {
throw new IllegalArgumentException(String.format("nThreads: %d (expected: > 0)", nThreads));
} if (executor == null) {
executor = new ThreadPerTaskExecutor(newDefaultThreadFactory());
} children = new EventExecutor[nThreads]; for (int i = 0; i < nThreads; i ++) {
boolean success = false;
try {
// 创建EventLoop
children[i] = newChild(executor, args);
success = true;
} catch (Exception e) {
// TODO: Think about if this is a good exception type
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) {
// Let the caller handle the interruption.
Thread.currentThread().interrupt();
break;
}
}
}
}
} // 初始化chooser,决定选择下一个线程的策略
chooser = chooserFactory.newChooser(children); 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);
} Set<EventExecutor> childrenSet = new LinkedHashSet<EventExecutor>(children.length);
Collections.addAll(childrenSet, children);
readonlyChildren = Collections.unmodifiableSet(childrenSet);
}

使用默认参数构造参数的话,上面这个构造方法的入参的值分别是

nThreads

// 默认的线程池大小
private static final int DEFAULT_EVENT_LOOP_THREADS; static {
// 如果配置了io.netty.eventLoopThreads参数的话,先取该参数的值
// 如果没有配置上面的参数,则取机器处理器个数的2倍
// 如果上面算出的结果小于1则取1
DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt(
"io.netty.eventLoopThreads", Runtime.getRuntime().availableProcessors() * 2)); if (logger.isDebugEnabled()) {
logger.debug("-Dio.netty.eventLoopThreads: {}", DEFAULT_EVENT_LOOP_THREADS);
}
} // 默认没有指定线程池大小,取DEFAULT_EVENT_LOOP_THREADS
protected MultithreadEventLoopGroup(int nThreads, Executor executor, Object... args) {
super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, executor, args);
}

executor

默认没有指定executor,为null

chooserFactory

protected MultithreadEventExecutorGroup(int nThreads, Executor executor, Object... args) {
this(nThreads, executor, DefaultEventExecutorChooserFactory.INSTANCE, args);
}
// io.netty.util.concurrent.DefaultEventExecutorChooserFactory

使用默认的chooser,该类的主要功能是提供选择下一个线程的策略

public final class DefaultEventExecutorChooserFactory implements EventExecutorChooserFactory {
// 单例
public static final DefaultEventExecutorChooserFactory INSTANCE = new DefaultEventExecutorChooserFactory(); private DefaultEventExecutorChooserFactory() { } @SuppressWarnings("unchecked")
@Override
public EventExecutorChooser newChooser(EventExecutor[] executors) {
if (isPowerOfTwo(executors.length)) {
// 如果是2的幂次则使用这个chooser
return new PowerOfTowEventExecutorChooser(executors);
} else {
return new GenericEventExecutorChooser(executors);
}
} private static boolean isPowerOfTwo(int val) {
// 判断一个数是否2的幂,方法很巧妙
return (val & -val) == val;
} private static final class PowerOfTowEventExecutorChooser implements EventExecutorChooser {
private final AtomicInteger idx = new AtomicInteger();
private final EventExecutor[] executors; PowerOfTowEventExecutorChooser(EventExecutor[] executors) {
this.executors = executors;
} @Override
public EventExecutor next() {
// 如果是2的幂次个线程,可以使用位运算计算出下一个选出的线程的index
return executors[idx.getAndIncrement() & executors.length - 1];
}
} private static final class GenericEventExecutorChooser implements EventExecutorChooser {
private final AtomicInteger idx = new AtomicInteger();
private final EventExecutor[] executors; GenericEventExecutorChooser(EventExecutor[] executors) {
this.executors = executors;
} @Override
public EventExecutor next() {
// 使用求余的方法计算出下一个线程的index
return executors[Math.abs(idx.getAndIncrement() % executors.length)];
}
}
}

可以看出上面两个chooser计算出的最终结果是一致的,但是使用位运算更快一点,所以如果是线程池的大小刚好是2的幂次的话使用位运算的chooser。

args

// args[0],下面方法返回的provider,在linux平台上默认是EPollSelectorProvider
java.nio.channels.spi.SelectorProvider#provider
// args[1],决定eventLoop每次执行select还是执行队列中的任务
io.netty.channel.DefaultSelectStrategyFactory
// args[2],等待队列满以后的拒绝策略
io.netty.util.concurrent.RejectedExecutionHandlers#REJECT

初始化NioEventLoopGroup过程主要是为了初始化线程池中每一个NioEventLoop,而每一个NioEventLoop包含一个selector。

初始化selector

接着上面说到的初始化NioEventLoop,调用newChild方法来初始化

// io.netty.channel.nio.NioEventLoopGroup#newChild
protected EventLoop newChild(Executor executor, Object... args) throws Exception {
// 下面这几个参数上面已经介绍过
return new NioEventLoop(this, executor, (SelectorProvider) args[0],
((SelectStrategyFactory) args[1]).newSelectStrategy(), (RejectedExecutionHandler) args[2]);
} NioEventLoop(NioEventLoopGroup parent, Executor executor, SelectorProvider selectorProvider,
SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler) {
// 调用父类构造方法初始化taskQueue,taskQueue的大小取Math.max(16, maxPendingTasks)
super(parent, executor, false, DEFAULT_MAX_PENDING_TASKS, rejectedExecutionHandler);
// 校验selectorProvider
if (selectorProvider == null) {
throw new NullPointerException("selectorProvider");
}
// 校验EventLoop每次执行的select策略是否为空
if (strategy == null) {
throw new NullPointerException("selectStrategy");
}
provider = selectorProvider;
// 初始化selector
selector = openSelector();
selectStrategy = strategy;
} private Selector openSelector() {
final Selector selector;
try {
// 调用的是sun.nio.ch.EPollSelectorProvider#openSelector
// 返回的是sun.nio.ch.EPollSelectorImpl
selector = provider.openSelector();
} catch (IOException e) {
throw new ChannelException("failed to open a new selector", e);
} // 是否使用SelectedSelectionKeySet优化,默认不禁用false
if (DISABLE_KEYSET_OPTIMIZATION) {
return selector;
} // Netty优化过后的
final SelectedSelectionKeySet selectedKeySet = new SelectedSelectionKeySet(); // 尝试获取SelectorImpl对象,后续会使用反射操作这个类的属性
Object maybeSelectorImplClass = AccessController.doPrivileged(new PrivilegedAction<Object>() {
@Override
public Object run() {
try {
return Class.forName(
"sun.nio.ch.SelectorImpl",
false,
PlatformDependent.getSystemClassLoader());
} catch (ClassNotFoundException e) {
return e;
} catch (SecurityException e) {
return e;
}
}
}); // 确保有权限访问该类
if (!(maybeSelectorImplClass instanceof Class) ||
// ensure the current selector implementation is what we can instrument.
!((Class<?>) maybeSelectorImplClass).isAssignableFrom(selector.getClass())) {
if (maybeSelectorImplClass instanceof Exception) {
Exception e = (Exception) maybeSelectorImplClass;
logger.trace("failed to instrument a special java.util.Set into: {}", selector, e);
}
return selector;
} final Class<?> selectorImplClass = (Class<?>) maybeSelectorImplClass; Object maybeException = AccessController.doPrivileged(new PrivilegedAction<Object>() {
@Override
public Object run() {
try {
// 得到字段selectedKeys
Field selectedKeysField = selectorImplClass.getDeclaredField("selectedKeys");
// 得到字段publicSelectedKeys
Field publicSelectedKeysField = selectorImplClass.getDeclaredField("publicSelectedKeys"); selectedKeysField.setAccessible(true);
publicSelectedKeysField.setAccessible(true); // 将selectedKeys、publicSelectedKeys均设置为Netty自定义的SelectedSelectionKeySet
selectedKeysField.set(selector, selectedKeySet);
publicSelectedKeysField.set(selector, selectedKeySet);
return null;
} catch (NoSuchFieldException e) {
return e;
} catch (IllegalAccessException e) {
return e;
} catch (RuntimeException e) {
// JDK 9 can throw an inaccessible object exception here; since Netty compiles
// against JDK 7 and this exception was only added in JDK 9, we have to weakly
// check the type
if ("java.lang.reflect.InaccessibleObjectException".equals(e.getClass().getName())) {
return e;
} else {
throw e;
}
}
}
}); if (maybeException instanceof Exception) {
selectedKeys = null;
Exception e = (Exception) maybeException;
logger.trace("failed to instrument a special java.util.Set into: {}", selector, e);
} else {
selectedKeys = selectedKeySet;
logger.trace("instrumented a special java.util.Set into: {}", selector);
} return selector;
}

初始化selector的过程中主要做了几件事:

  • 使用平台相关的provider初始化对应的SelectorImpl,这里使用了Java的SPI来加载平台相关的provider,每一种provider又对应一种SelectorImpl
  • 如果没有禁用selectedKey优化,Netty会使用自定的SelectedSelectionKeySet替换SelectorImpl的publicSelectedKeys、selectedKeys

对SelectorImpl.selectedKey优化的说明

  1. 利用反射将SelectorImpl.selectedKey替换成了SelectedSelectionKeySet,SelectedSelectionKeySet利用数组实现元素存放
  2. 在调用select方法的时候如果有事件进来的时候会调用SelectedSelectionKeySet#add,将有IO事件的selectKey添加到keyset中
  3. 使用数组遍历(processSelectedKeysOptimized)要比使用set遍历快一些,参考文后第一篇参考文章
  4. 在Java9以后这个优化就失效了,因为Java9引入了Jigsaw

接下来看看Selector创建过程,上面调用了EPollSelectorProvider#openSelector来开始初始化selector

public AbstractSelector openSelector() throws IOException {
// 直接new 一个EPollSelectorImpl
return new EPollSelectorImpl(this);
} // 该构造方法只能是包内使用,供provider来调用
EPollSelectorImpl(SelectorProvider sp) throws IOException {
// 调用父类SelectorImpl的构造方法初始化selectedKeys、publicKeys、publicSelectedKeys
// 上面已经说过了,如果使用Netty的优化,publicKeys、publicSelectedKey会被替换
super(sp);
// 调用linux的pipe方法,创建一个管道,配置为非阻塞的
long pipeFds = IOUtil.makePipe(false);
// 高32为读文件描述符
fd0 = (int) (pipeFds >>> 32);
// 低32位为写文件描述符
fd1 = (int) pipeFds;
// EPollArrayWrapper包含一系列native方法来调用EPollArrayWrapper.c本地方法
pollWrapper = new EPollArrayWrapper();
pollWrapper.initInterrupt(fd0, fd1);
// fdToKey用来保存文件描述符和SelectionKeyImpl的映射
fdToKey = new HashMap<>();
} EPollArrayWrapper() throws IOException {
// creates the epoll file descriptor
// 创建epoll的文件描述符
epfd = epollCreate(); // the epoll_event array passed to epoll_wait
int allocationSize = NUM_EPOLLEVENTS * SIZE_EPOLLEVENT;
pollArray = new AllocatedNativeObject(allocationSize, true);
pollArrayAddress = pollArray.address(); // eventHigh needed when using file descriptors > 64k
if (OPEN_MAX > MAX_UPDATE_ARRAY_SIZE)
eventsHigh = new HashMap<>();
}

终于看到创建epoll文件描述符相关代码了,上面这个还是看不到究竟调用了哪些本地方法,我们看看相关的c代码

// jdk/src/solaris/native/sun/nio/ch/IOUtil.c
JNIEXPORT jlong JNICALL
Java_sun_nio_ch_IOUtil_makePipe(JNIEnv *env, jobject this, jboolean blocking)
{
int fd[2]; // 打开pipe
if (pipe(fd) < 0) {
JNU_ThrowIOExceptionWithLastError(env, "Pipe failed");
return 0;
}
if (blocking == JNI_FALSE) {
// 配置管道为非阻塞
if ((configureBlocking(fd[0], JNI_FALSE) < 0)
|| (configureBlocking(fd[1], JNI_FALSE) < 0)) {
JNU_ThrowIOExceptionWithLastError(env, "Configure blocking failed");
close(fd[0]);
close(fd[1]);
return 0;
}
}
// 将读写文件描述符放入一个long型中返回
return ((jlong) fd[0] << 32) | (jlong) fd[1];
} // jdk/src/solaris/native/sun/nio/ch/EPollArrayWrapper.c
JNIEXPORT jint JNICALL
Java_sun_nio_ch_EPollArrayWrapper_epollCreate(JNIEnv *env, jobject this)
{
/*
* epoll_create expects a size as a hint to the kernel about how to
* dimension internal structures. We can't predict the size in advance.
*/
// 这里调用linux函数epoll_create创建epoll的文件描述符
int epfd = epoll_create(256);
if (epfd < 0) {
JNU_ThrowIOExceptionWithLastError(env, "epoll_create failed");
}
return epfd;
}

总结

经过上面说明,现在对于Netty启动过程中线程池的初始化过程和selector初始化过程已经比较清晰了,对于native方法的分析让我们对比linux中epoll编程,对于原理更加清楚。

接下来就是将需要监听的描述符注册到epoll上,对应到Netty就是讲channel注册到selector上,下一篇文章继续写Netty源码—二、server启动(2)

参考

Netty源码分析——Reactor的processSelectedKeys

关于SelectedSelectionKeySet优化的讨论

https://github.com/netty/netty/issues/2363

https://github.com/netty/netty/commit/cd579f75d2b5f236f35bc47f454cc07e50ae8037

Netty源码—一、server启动(1)的更多相关文章

  1. Netty源码解析—客户端启动

    Netty源码解析-客户端启动 Bootstrap示例 public final class EchoClient { static final boolean SSL = System.getPro ...

  2. Netty源码阅读(一) ServerBootstrap启动

    Netty源码阅读(一) ServerBootstrap启动 转自我的Github Netty是由JBOSS提供的一个java开源框架.Netty提供异步的.事件驱动的网络应用程序框架和工具,用以快速 ...

  3. Netty 源码(一)服务端启动

    Netty 源码(一)服务端启动 Netty 系列目录(https://www.cnblogs.com/binarylei/p/10117436.html) ServerBootstap 创建时序图如 ...

  4. Netty源码分析第1章(Netty启动流程)---->第1节: 服务端初始化

    Netty源码分析第一章:  Server启动流程 概述: 本章主要讲解server启动的关键步骤, 读者只需要了解server启动的大概逻辑, 知道关键的步骤在哪个类执行即可, 并不需要了解每一步的 ...

  5. Netty源码分析第1章(Netty启动流程)---->第2节: NioServerSocketChannel的创建

    Netty源码分析第一章:  Server启动流程 第二节:NioServerSocketChannel的创建 我们如果熟悉Nio, 则对channel的概念则不会陌生, channel在相当于一个通 ...

  6. Netty源码分析第1章(Netty启动流程)---->第5节: 绑定端口

    Netty源码分析第一章:Netty启动步骤 第五节:绑定端口 上一小节我们学习了channel注册在selector的步骤, 仅仅做了注册但并没有监听事件, 事件是如何监听的呢? 我们继续跟第一小节 ...

  7. Netty源码分析第2章(NioEventLoop)---->第4节: NioEventLoop线程的启动

    Netty源码分析第二章: NioEventLoop   第四节: NioEventLoop线程的启动 之前的小节我们学习了NioEventLoop的创建以及线程分配器的初始化, 那么NioEvent ...

  8. Netty源码分析 (三)----- 服务端启动源码分析

    本文接着前两篇文章来讲,主要讲服务端类剩下的部分,我们还是来先看看服务端的代码 /** * Created by chenhao on 2019/9/4. */ public final class ...

  9. Netty源码剖析-启动服务

    参考文献:极客时间傅健老师的<Netty源码剖析与实战>Talk is cheap.show me the code! --1主线分两步: 一:首先在our thread里,如果写在mai ...

随机推荐

  1. 解锁 vmware esxi 6.7 并安装 mac os 10.13

    1.安装 esxi 6.7 2.下载 unlocker 2.1.1.zip 3.上传 unlocker 2.1.1.zip esxi的磁盘中 4.开启esxi的ssh登录 5.使用 ssh 登录 es ...

  2. bzoj 2428 均分数据 模拟退火

    模拟退火 按照自己的思路打了,结果WA,发现退火最关键的就是初温,降温,和修改次数, 这个题还在外层带了一个循环,骚气 #include<cstdio> #include<iostr ...

  3. 【dfs+连通分量】Bzoj1123 POI2008 BLO

    Description Byteotia城市有n个 towns m条双向roads. 每条 road 连接 两个不同的 towns ,没有重复的road. 所有towns连通. Input 输入n&l ...

  4. 在线数据库表(sql语句)生成java实体类工具

    相信每个做java开发的读者,都接触过SQL建表语句,尤其是在项目开发初期,因为数据库是项目的基石. 在现代项目开发中,出现了许多ORM框架,通过简单的实体映射,即可实现与数据库的交互,然而我们最初设 ...

  5. ionic3 懒加载在微信上缓存的问题

    1.懒加载是什么? 在ionic2中所有的组件.模块.服务.管道等都堆积在app.module.ts模块中,在页面初始化的时候会一次性加载所有的资源,导致资源过大,页面渲染缓慢,也导致app.modu ...

  6. 【爆料】-《西悉尼大学毕业证书》UWS一模一样原件

    ☞西悉尼大学毕业证书[微/Q:865121257◆WeChat:CC6669834]UC毕业证书/联系人Alice[查看点击百度快照查看][留信网学历认证&博士&硕士&海归&a ...

  7. Hadoop配置第1节-集群网络配置

    Hadoop-集群网络配置 总体目标:完成zookeeper+Hadoop+Hbase 整合平台搭建   进度:1:集群网络属性配置2:集群免密码登陆配置3:JDK的安装4:Zookeeper的安装5 ...

  8. java泛型中使用的排序算法——归并排序及分析

    一.引言 我们知道,java中泛型排序使用归并排序或TimSort.归并排序以O(NlogN)最坏时间运行,下面我们分析归并排序过程及分析证明时间复杂度:也会简述为什么java选择归并排序作为泛型的排 ...

  9. Promise原理—一步一步实现一个Promise

    promise特点 一个promise的当前状态只能是pending.fulfilled和rejected三种之一.状态改变只能是pending到fulfilled或者pending到rejected ...

  10. socketserver实现并发

    socketserver实现并发原理:给每一个前来链接的客户端开启一个线程执行通信.也就是给每一个连接“配备”了一个管家. 下面用一个简单的示例来演示socketserver实现并发(一个服务端,两个 ...