Netty-ChannelHandler并发安全分析

ChannelHandler是Netty中使用最广的接口,Netty提供了大量内置的ChannelHandler实现类,包括编解码、SSL、日志打印等等。用户通过实现ChannelHandler接口,来接口和发送业务消息,并进行业务逻辑处理。


   Netty ChannelHandler并发安全问题
        串行执行的ChannelHandler
        跨链路共享的ChannelHandler
        ChannelHandler并发场景总结


Netty 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

     如果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的工作原理:
                                       Netty-ChannelHandler并发安全分析_第1张图片


     那么问题来了,如果只初始化一次业务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

     如果某个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并发场景总结

     用户自定义的ChannelHandler有两种场景需要考虑并发安全:

  1. Sharable注解,多个ChannelPipeline共享ChannelHandler,它将多个NioEventLoop线程并发访问:
                                  Netty-ChannelHandler并发安全分析_第2张图片
                                                                    截自《Netty进阶之路》
         这种情况用户需要保证ChannelHandler的并发安全,尽量使用原子类等方式降低锁的开销,防止阻塞NioEventLoop线程。

2.ChannelHandler没有共享, 但在用户的ChannelPipeline中的一些ChannelHandler绑定了新的线程池,这样ChannelPipeline的ChannelHandler就会被异步执行。
                                           Netty-ChannelHandler并发安全分析_第3张图片
                                                                     ChannelHandler绑定线程池相关接口
     在多线程异步执行过程中,如果某ChannelHandler的成员变量共享给其他ChannelHandler,那么多个被多个线程并发访问和修改就存在并发问题,如图:
                                             Netty-ChannelHandler并发安全分析_第4张图片
                                                                               截自《Netty进阶之路》

你可能感兴趣的:(Netty,java,netty,多线程)