优雅的关闭
java的优雅关闭通常通过注册 jdk 的shutdownHook 来实现。
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
System.out.println("shutdown start...");
// do your work
System.out.println("shutdown end...");
}
});
System.exit(0);
在应用程序优雅退出的时候,netty也需要优雅退出。netty提供了 shutdownGracefully() 来实现。在 客户端的创建 中,我们可以看到netty提供的案例中,在finally里调用了shutdownGracefully()
public final class EchoClient {
public static void main(String[] args) throws Exception {
// 省略了部分代码
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
// 省略了部分代码
p.addLast(new EchoClientHandler());
}
});
ChannelFuture f = b.connect(HOST, PORT).sync();
f.channel().closeFuture().sync();
} finally {
// Shut down the event loop to terminate all threads.
group.shutdownGracefully();
}
}
}
分析其源码:
1、NioEventLoopGroup
NioEventLoopGroup其实就是 NioEventLoop线程组,它的实现就是循环调用NioEventLoop的shutdownGracefully()。(在MultithreadEventExecutorGroup 中)
@Override
public Future> shutdownGracefully(long quietPeriod, long timeout, TimeUnit unit) {
for (EventExecutor l: children) {
l.shutdownGracefully(quietPeriod, timeout, unit);
}
return terminationFuture();
}
2、NioEventLoop
通过分析 NioEventLoop 的 run() 方法,可以看到 NioEventLoop运行的时候根据线程状态来判断是否需要进行退出操作。
@Override
protected void run() {
for (;;) {
// 省略部分代码: NioEventLoop 运行时处理的逻辑
// Always handle shutdown even if the loop processing threw an exception.
try {
// 通过修改线程状态判断是否关闭。修改线程状态具体实现在父类 SingleThreadEventExecutor.shutdownGracefully() 中
// 如果正在关闭中,则执行下列代码
if (isShuttingDown()) {
closeAll();
// 关闭完成之后,判断是否真的可以退出。关闭连接,释放资源之后,NioEventLoop 还有扫尾工作需要进行。
// NioEventLoop 除了IO操作,还需要负责执行定时任务,shutDownHook 的执行等.
// 此时如果有到期的定时任务,即使channel已经关闭也要执行,线程不能退出
if (confirmShutdown()) {
return;
}
}
} catch (Throwable t) {
handleLoopException(t);
}
}
}
@Override
public boolean isShuttingDown() {
return state >= ST_SHUTTING_DOWN;
}
通过线程状态 state 判断线程是否正在关闭。在其父类 SingleThreadEventExecutor.shutdownGracefully() 中(正是上文 NioEventLoopGroup中循环调用的方法)进行了对线程状态的修改,将 state 改为 ST_SHUTTING_DOWN
@Override
public Future> shutdownGracefully(long quietPeriod, long timeout, TimeUnit unit) {
if (isShuttingDown()) {
return terminationFuture();
}
// 判断当前线程是否是eventLoop绑定的线程,这样的判断在 NioEventLoop各种流程中都很常见。
// 在客户端的创建一文中可以看到一个channel绑定了一个专门的NioEventLoop来处理channel中的各种事件,一个NioEventLoop则有一个专门的线程来执行。
// 在整个channel的生命周期内,所有的操作都将由这个线程来执行,如果当前线程不是eventLoop绑定的线程,则会提交给eventLoop绑定的队列,等待绑定线程执行。eventLoop都实现了java的ScheduledExecutorService接口
// 一个NioEventLoop可以被多个channel同时使用。如果是一一对应,则相当于是BIO模型,一个channel对应一个线程
boolean inEventLoop = inEventLoop();
boolean wakeup;
int oldState;
// 通过自旋和 cas 的方式修改线程状态
for (;;) {
// 自旋中首先判断线程状态是否已经被其他线程修改
if (isShuttingDown()) {
return terminationFuture();
}
int newState;
wakeup = true;
oldState = state;
if (inEventLoop) {
newState = ST_SHUTTING_DOWN;
} else {
switch (oldState) {
case ST_NOT_STARTED:
case ST_STARTED:
newState = ST_SHUTTING_DOWN;
break;
default:
newState = oldState;
wakeup = false;
}
}
// 如果没有其他线程修改 state, 则通过 cas 的方式修改 state
if (STATE_UPDATER.compareAndSet(this, oldState, newState)) {
break;
}
}
gracefulShutdownQuietPeriod = unit.toNanos(quietPeriod);
gracefulShutdownTimeout = unit.toNanos(timeout);
if (oldState == ST_NOT_STARTED) {
thread.start();
}
if (wakeup) {
wakeup(inEventLoop);
}
return terminationFuture();
}
因为 shutdownGracefully() 可能由 NIOEventLoopGroup 发起调用,也可能由多个用户线程发起调用,所以通过自旋加 cas 的方式进行并发控制。
回到上文中 NioEventLoop 的 run() 方法,可以看到当线程状态 state >= ST_SHUTTING_DOWN 时,执行了closeAll() 方法。closeAll() 执行完成之后,通过confirmShutdown()判断是否真的可以退出,其具体作用可以看代码中注释。
我们先分析 closeAll()的源码。
private void closeAll() {
// 更新 selectionKey
selectAgain();
Set keys = selector.keys();
Collection channels = new ArrayList(keys.size());
for (SelectionKey k: keys) {
Object a = k.attachment();
if (a instanceof AbstractNioChannel) {
// 将所有注册在selector 上的 channel 循环关闭
channels.add((AbstractNioChannel) a);
} else {
k.cancel();
@SuppressWarnings("unchecked")
// 如果不是channel,则将task关联的channel状态置为 Unregistered
NioTask task = (NioTask) a;
invokeChannelUnregistered(task, k, null);
}
}
for (AbstractNioChannel ch: channels) {
// 具体实现在 AbstractChannel.AbstractUnsafe 中
ch.unsafe().close(ch.unsafe().voidPromise());
}
}
这里为什么 不是channel 就是 NioTask,暂时还不是很清楚这方面的细节。
上述代码将 注册在selector 上的 channel循环调用,具体继续关注 AbstractChannel.AbstractUnsafe。
private void close(final ChannelPromise promise, final Throwable cause,
final ClosedChannelException closeCause, final boolean notify) {
if (!promise.setUncancellable()) {
return;
}
if (closeInitiated) {
if (closeFuture.isDone()) {
// Closed already.
safeSetSuccess(promise);
} else if (!(promise instanceof VoidChannelPromise)) { // Only needed if no VoidChannelPromise.
// This means close() was called before so we just register a listener and return
closeFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
promise.setSuccess();
}
});
}
return;
}
closeInitiated = true;
final boolean wasActive = isActive();
final ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
// 将outboundBuffer 置为 null ,不再允许发送新的消息
this.outboundBuffer = null; // Disallow adding any messages and flushes to outboundBuffer.
Executor closeExecutor = prepareToClose();
if (closeExecutor != null) {
closeExecutor.execute(new Runnable() {
@Override
public void run() {
try {
// Execute the close.
doClose0(promise);
} finally {
// Call invokeLater so closeAndDeregister is executed in the EventLoop again!
invokeLater(new Runnable() {
@Override
public void run() {
if (outboundBuffer != null) {
// Fail all the queued messages
outboundBuffer.failFlushed(cause, notify);
// 释放发送队列中还没有发送完成的 byteBuf,等待 gc
outboundBuffer.close(closeCause);
}
// 1、将链路上的channel 置为 inactive 状态 (channel 的生命周期: channelRegistered:channel 被注册到了eventLoop -> channelActive:channel处于活动状态,可以接收发送数据
// -> channelInactive: channel没有连接到远程节点 -> channelRegistered: channel已经被创建但没有注册到eventLoop)
// 2、从多路复用器上取消 selectionKey
fireChannelInactiveAndDeregister(wasActive);
}
});
}
}
});
} else {
try {
// Close the channel and fail the queued messages in all cases.
// 关闭 channel,将对应的channelFuture操作结果置为失败
doClose0(promise);
} finally {
if (outboundBuffer != null) {
// Fail all the queued messages.
outboundBuffer.failFlushed(cause, notify);
outboundBuffer.close(closeCause);
}
}
// 判断当前链路是否有消息正在发送,如果有,则稍后再执行
if (inFlush0) {
invokeLater(new Runnable() {
@Override
public void run() {
fireChannelInactiveAndDeregister(wasActive);
}
});
} else {
//
fireChannelInactiveAndDeregister(wasActive);
}
}
}
这段代码较为复杂,其中主要做了这几个操作:
1、将outboundBuffer 置为 null ,不再允许发送新的消息
2、doClose0(promise); 关闭channel,将对应的channelFuture操作结果置为失败
3、outboundBuffer.close(closeCause); 释放发送队列中还没有发送完成的 byteBuf,等待 gc
4、fireChannelInactiveAndDeregister(wasActive);
4.1、将链路上的channel 置为 inactive 状态 (channel 的生命周期: channelRegistered:channel 被注册到了 eventLoop -> channelActive:channel处于活动状态,可以接收发送数据
-> channelInactive: channel没有连接到远程节点 -> channelRegistered: channel已经被创建但没有注 册到eventLoop);
4.2、从多路复用器上取消 selectionKey
其中NioSocketChannel 的 doClose0源码实现如下
@Override
protected void doClose() throws Exception {
super.doClose();
// 关闭 java channel
javaChannel().close();
}
其父类 AbstractNioChannel 的实现
@Override
protected void doClose() throws Exception {
ChannelPromise promise = connectPromise;
if (promise != null) {
// Use tryFailure() instead of setFailure() to avoid the race against cancel().
promise.tryFailure(DO_CLOSE_CLOSED_CHANNEL_EXCEPTION);
connectPromise = null;
}
// 只有当设置了connection timeout 的时候,在connect 的时候会创建 connectTimeoutFuture
ScheduledFuture> future = connectTimeoutFuture;
if (future != null) {
//
future.cancel(false);
connectTimeoutFuture = null;
}
}
// 具体实现在DefaultPromise类
@Override
public boolean tryFailure(Throwable cause) {
if (setFailure0(cause)) {
notifyListeners();
return true;
}
return false;
}
其中setFailure0(cause) 最终调用
private boolean setValue0(Object objResult) {
if (RESULT_UPDATER.compareAndSet(this, null, objResult) ||
RESULT_UPDATER.compareAndSet(this, UNCANCELLABLE, objResult)) {
checkNotifyWaiters();
return true;
}
return false;
}
channelFuture,继承java 的 future接口,是异步操作的结果,其中用一个 result 表示异步操作的状态。channelPromise继承自channelFuture,可以通过 setSuccess(), setFailure(),trySuccess(), tryFailure()等方法标记future的result并通知注册的监听器。其中try 和 set 的区别是 try 会返回操作是否成功,失败返回false,而 set 失败则抛异常。
doClose0源码分析完成,接下来分析 fireChannelInactiveAndDeregister(wasActive) 的源码。里面主要调用了
private void deregister(final ChannelPromise promise, final boolean fireChannelInactive) {
// 省略部分代码
invokeLater(new Runnable() {
@Override
public void run() {
try {
// 从多路复用器上取消 selectionKey,具体实现在 AbstractNioChannel
doDeregister();
} catch (Throwable t) {
logger.warn("Unexpected exception occurred while deregistering a channel.", t);
} finally {
if (fireChannelInactive) {
// 将channel的状态置为 inactive,触发链路关闭通知事件,触发链路上所有 channel 的对应channelHandler的对应方法
pipeline.fireChannelInactive();
}
// Some transports like local and AIO does not allow the deregistration of
// an open channel. Their doDeregister() calls close(). Consequently,
// close() calls deregister() again - no need to fire channelUnregistered, so check
// if it was registered.
if (registered) {
registered = false;
// 将channel的状态置为 Unregistered
pipeline.fireChannelUnregistered();
}
safeSetSuccess(promise);
}
}
});
}
@Override
protected void doDeregister() throws Exception {
// 从多路复用器上取消 selectionKey,具体实现调用的是 java nio的selectionKey的cancel(),这里不做展开
eventLoop().cancel(selectionKey());
}
至此,closeAll() 方法分析完成。让我们回到 NioEventLoop 的 run() 方法,接下来看下 confirmShutdown()的源码
protected boolean confirmShutdown() {
if (!isShuttingDown()) {
return false;
}
if (!inEventLoop()) {
throw new IllegalStateException("must be invoked from an event loop");
}
// 取消scheduledTaskQueue中的所有定时任务 cancelWithoutRemove
cancelScheduledTasks();
if (gracefulShutdownStartTime == 0) {
gracefulShutdownStartTime = ScheduledFutureTask.nanoTime();
}
// runAllTasks 执行在 taskQueue中排队的task(会将 scheduledTaskQueue 中执行时间满足条件的定时任务也添加到 taskQueue中), 只有至少一个任务成功执行才返回true
// runShutdownHooks 执行注册到 NioEventLoop 中的 shutdownHook。shutdownHook是 jdk 优雅退出的一个钩子,在jvm 因为某些情况退出的时候可以被调用,用于我们在关闭时处理自己的逻辑
// 在 doRegistered 的时候就会 addShutdownHook
if (runAllTasks() || runShutdownHooks()) {
if (isShutdown()) {
// Executor shut down - no new tasks anymore.
return true;
}
// There were tasks in the queue. Wait a little bit more until no tasks are queued for the quiet period or
// terminate if the quiet period is 0.
// See https://github.com/netty/netty/issues/4241
if (gracefulShutdownQuietPeriod == 0) {
return true;
}
wakeup(true);
return false;
}
final long nanoTime = ScheduledFutureTask.nanoTime();
// 判断是否到达优雅关闭指定的超时时间
if (isShutdown() || nanoTime - gracefulShutdownStartTime > gracefulShutdownTimeout) {
return true;
}
// 如果没有到达超时时间,则暂时不关闭,每隔100ms检测是否有新的任务加入,有则执行
if (nanoTime - lastExecutionTime <= gracefulShutdownQuietPeriod) {
wakeup(true);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// Ignore
}
return false;
}
// No tasks were added for last quiet period - hopefully safe to shut down.
// (Hopefully because we really cannot make a guarantee that there will be no execute() calls by a user.)
// 不能完全确保所有消息都能够处理完成
return true;
}
其中,首先cancelScheduledTasks()
protected void cancelScheduledTasks() {
assert inEventLoop();
Queue> scheduledTaskQueue = this.scheduledTaskQueue;
if (isNullOrEmpty(scheduledTaskQueue)) {
return;
}
final ScheduledFutureTask>[] scheduledTasks =
scheduledTaskQueue.toArray(new ScheduledFutureTask>[scheduledTaskQueue.size()]);
for (ScheduledFutureTask> task: scheduledTasks) {
// 将future 的result置为 cancel,并通知监听器
task.cancelWithoutRemove(false);
}
// 这里清空了scheduledTaskQueue里的元素
scheduledTaskQueue.clear();
}
然后, runAllTasks(),将任务从 scheduledTaskQueue中取出,添加到 taskQueue中。如果taskQueue内存不足,则再放回scheduledTaskQueue,在之后的循环中再取出。
protected boolean runAllTasks() {
boolean fetchedAll;
do {
fetchedAll = fetchFromScheduledTaskQueue();
Runnable task = pollTask();
if (task == null) {
return false;
}
for (;;) {
try {
task.run();
} catch (Throwable t) {
logger.warn("A task raised an exception.", t);
}
task = pollTask();
if (task == null) {
break;
}
}
} while (!fetchedAll); // keep on processing until we fetched all scheduled tasks.
lastExecutionTime = ScheduledFutureTask.nanoTime();
return true;
}
// 将任务从 scheduledTaskQueue中取出,offer 到 taskQueue中
private boolean fetchFromScheduledTaskQueue() {
long nanoTime = AbstractScheduledEventExecutor.nanoTime();
Runnable scheduledTask = pollScheduledTask(nanoTime);
while (scheduledTask != null) {
if (!taskQueue.offer(scheduledTask)) {
// No space left in the task queue add it back to the scheduledTaskQueue so we pick it up again.
// taskQueue offer 失败,说明没有足够的空间添加,则将任务放回 scheduledTaskQueue, 在之后的循环会再次从 scheduledTaskQueue 中取出这个任务
scheduledTaskQueue().add((ScheduledFutureTask>) scheduledTask);
return false;
}
scheduledTask = pollScheduledTask(nanoTime);
}
return true;
}
// 只会取出执行时间在彻底关闭之前的任务
protected final Runnable pollScheduledTask(long nanoTime) {
assert inEventLoop();
Queue> scheduledTaskQueue = this.scheduledTaskQueue;
ScheduledFutureTask> scheduledTask = scheduledTaskQueue == null ? null : scheduledTaskQueue.peek();
if (scheduledTask == null) {
return null;
}
// 只会取出执行时间在彻底关闭之前的任务
if (scheduledTask.deadlineNanos() <= nanoTime) {
scheduledTaskQueue.remove();
return scheduledTask;
}
return null;
}
这里有一点一直没想明白。在 cancelScheduledTasks() 中已经调用 scheduledTaskQueue.clear(); 在这里却又从scheduledTaskQueue中取出 task 来执行。
最后,runShutdownHooks()
private boolean runShutdownHooks() {
boolean ran = false;
// Note shutdown hooks can add / remove shutdown hooks.
while (!shutdownHooks.isEmpty()) {
List copy = new ArrayList(shutdownHooks);
shutdownHooks.clear();
for (Runnable task: copy) {
try {
task.run();
} catch (Throwable t) {
logger.warn("Shutdown hook raised an exception.", t);
} finally {
ran = true;
}
}
}
if (ran) {
lastExecutionTime = ScheduledFutureTask.nanoTime();
}
return ran;
}
循环执行我们事先添加的 shutdownHook。可以通过下面的方式添加自己需要的shutdownHook
((SingleThreadEventExecutor)eventLoop()).addShutdownHook(new Runnable());
===============================================
真正的菜鸟一枚,边学习边整理中,可能有错漏