ChannelHandler是Netty中使用最广的接口,Netty提供了大量内置的ChannelHandler实现类,包括编解码、SSL、日志打印等等。用户通过实现ChannelHandler接口,来接口和发送业务消息,并进行业务逻辑处理。
Netty ChannelHandler并发安全问题
串行执行的ChannelHandler
跨链路共享的ChannelHandler
ChannelHandler并发场景总结
业务有一个非线程安全的类ThreadUnsafeClass,这个类会在业务ChannelHandler的channelRead方法中被调用。下面这样的调用方法在多线程环境是否安全:
public class ServiceHandler extends ChannelInboundHandlerAdapter{
private ThreadUnsafeClass unsafe = new ThreadUnsafeClass(); //非线程安全类
public void channelRead(ChannelHandlerContext ctx, Object msg){
//此处调用是否正确?
unsafe.doSomething(ctx, msg);
}
}
如果ChannelHandler是非共享的,则它就是线程安全的,原因:当链路完成初始化会创建ChannelPipeline,每个channel对应一个ChannelPipeline实例,业务的ChannelHandler会被实例化并加入ChannelPipeline中执行,由于某个Channel只能被特定的NioEventLoop线程执行,因此ChannelHandler不会被并发调用,不用考虑线程安全问题,相关源码:
.handler(new ChannelInitializer<SocketChannel>(){
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(
new NoThreadSecurityClientHandler()
);
}
});
业务可能会多线程调用ChannelHandlerContext或者Channel的write方法,这会不会导致多个业务线程并发调用ChannelHandler呢?答案是并不会产生并发安全问题。在执行write操作时,会判断是否是下一个要执行wirte操作的AbstractChannelHandlerContext的EventExecutor线程,如果不是则将write操作封装成AbstractWriteTask放入线程任务队列异步执行,原调用线程返回。如果业务的ChannelHandler没有指定EventExecutor,则使用的就是消息读写对应的NioEventLoop线程。因此即便多个业务线程并发调用某个Channel,也不会产生多个线程并发访问业务ChannelHandler的问题。源码如下(AbstractChannelHandlerContext类):
private void write(Object msg, boolean flush, ChannelPromise promise){
AbstractChannelHandlerContext next = findContextOutbound();
final Object m = pipeline.touch(msg, next);
EventExecutor executor = next.executor();
if(executor.inEventLoop()){
if(flush){
next.invokeWriteAndFlush(m, promise);
}else{
next.invokeWrite(m, promise);
}
}else{
AbstractWriteTask task;
if(flush){
task = WriteAndFlushTask.newInstance(next, m, promise);
}else{
task = WriteTask.newInstance(next, m, promise);
}
safeExecute(executor, task, promise, m);
}
}
那么问题来了,如果只初始化一次业务ChannelHandler,然后加到多个Channel的ChannelPipeline,由于不同的Channel可能绑定不同的NioEventLoop线程,这样ChannelHandler就可能被多个I/O线程访问,存在并发访问风险了。那实际情况如何呢,测试一下,更改客户端连接类:
public void connect() throws Exception{
EventLoopGroup group = new NioEventLoopGroup(4);
Bootstrap b = new Bootstrap();
//DemoHandler为测试并发的任意handler
ChannelHandler clientHandler = new DemoHandler();
b.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception{
ch.pipeline().addLast(clientHandler);
}
});
ChannelFuture f = null;
//模拟4个客户端启动访问,每个客户端链路都共用一个clientHandler
for(int i = 0; i < 4; i++){
f = b.connect(HOST, PORT).sync();
}
f.channel().closeFuture().sync();
group.shutdownGracefully();
}
模拟4个客户端并发访问服务端,每个客户端链路都共用一个clientHandler,运行结果报异常:io.netty.channel.ChannelPipelineException:clientHandler is not a @Sharable handler, so can’t be added or removed multiple times.
当把同一个ChannelHandler加入多个ChannelPipeline时会发生异常,相关源码:
private static void checkMultiplicity(ChannelPipeline handler){
if(handler instanceof ChannelHandlerAdapter){
ChannelHandlerAdapter h = (ChannelHandlerAdapter)handler;
if(!h.isSharable() && h.added){
throw new ChannelPipelineException(h.getClass().getName() + "is not a @Sharable handler, so can't be added or removed multiple times.");
}
h.added = true;
}
}
如果ChannelHandler不是共享的,重复向ChannelPipeline添加时会抛出ChannelPipelineException异常,添加失败。所以非共享的同一个ChannelHandler实例不能被重复加入多个ChannelPipeline或者被多次加入某一个ChannelPipeline。
如果某个ChannelHandler需要全局共享,则通过Sharable注解就可以被添加到多个ChannelPipeline,示例如下:
@ChannelHandler.Sharable
public class SharableClientHandler extends ChannelInboundHandlerAdapter{
}
对上一节代码做修改,如下:
@ChannelHandler.Sharable
public void connect() throws Exception{
EventLoopGroup group = new NioEventLoopGroup(4);
Bootstrap b = new Bootstrap();
ChannelHandler clientHandler = new SharableClientHandler();
//后续代码为同一channelHandler添加到多个pipeline
}
当ChannelHandler被添加到多个ChannelPipeline,就会面临多线程并发访问问题,需要ChannelHandler保证自身的线程安全,例如通过原子类、读写锁等方式对数据做并发保护。如果枷锁,可能会阻塞NioEventLoop线程,所以Sharable注解的ChannelHandler要慎用。
例如ChannelHandler是共享的,但它自身也不是线程安全的,就会导致并发问题,客户端代码示例(SharableClientHandler类):
@ChannelHandler.Sharable
public class SharableClientHandler extends ChannelInboundHandlerAdapter{
//变量共享,存在并发问题
int counter = 0;
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf msg = (ByteBuf) msg;
if(counter++ < 10000){
ctx.write(msg);
}
}
}
counter是成员变量,操作为非线程安全的,因此它无法保证精确地控制发送次数。进行一些优化,实现线程安全,代码如下:
@ChannelHandler.Sharable
public class SharableClientHandler extends ChannelInboundHandlerAdapter{
AtomicInteger counter = new AtomicInteger(0);
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf msg = (ByteBuf) msg;
if(counter.getAndIncrement() < 10000){
ctx.write(msg);
}
}
}
上述修改后并发访问依然能够保证逻辑正确性。当用户使用ChannelHandler.Sharable注解时,一定要谨慎,如果使用一定要保证ChannelHandler的线程安全性,保证程序的正确性。
用户自定义的ChannelHandler有两种场景需要考虑并发安全:
2.ChannelHandler没有共享, 但在用户的ChannelPipeline中的一些ChannelHandler绑定了新的线程池,这样ChannelPipeline的ChannelHandler就会被异步执行。
ChannelHandler绑定线程池相关接口
在多线程异步执行过程中,如果某ChannelHandler的成员变量共享给其他ChannelHandler,那么多个被多个线程并发访问和修改就存在并发问题,如图:
截自《Netty进阶之路》