注解说明
@Sharable 的作用其实非常简单,也不难理解,但是官方的说明有点难理解。
Indicates that the same instance of the annotated ChannelHandler can be added to one or more ChannelPipelines multiple times without a race condition.If this annotation is not specified, you have to create a new handler instance every time you add it to a pipeline because it has unshared state such as member variables.This annotation is provided for documentation purpose, just like the JCIP annotations.标识同一个ChannelHandler的实例可以被多次添加到多个ChannelPipelines中,而且不会出现竞争条件。如果一个ChannelHandler没有标志@Shareable,在添加到到一个pipeline中时,你需要每次都创建一个新的handler实例,因为它的成员变量是不可分享的。这个注解仅作为文档参考使用,比如说JCIP注解。
有了@Sharable 就一定保证了不会出现竞争条件? 测试证明这里 不太准确。官方的模糊说明,最为致命。WTF
经过很多很多的测试,发现它只对自定义的 Handler在添加到pipeline的时候 有一点作用。其实很简单,两个情况:
1 如果每次通过new 而不是共享的方式,那么加不加@Sharable 效果都是一样的。每个Channel使用不通的ChannelHandler 对象。
如在 .childHandler(new ChannelInitializer<SocketChannel>() { 中这样写:
pipeline().addLast(new EchoServerHandler());
这个方式是 每次都创建一个新的实例,其实就不会检查是否Sharable ,因为肯定是 unSharable
2 如果通过共享的方式,也就是 Handler 实例只有一个,那么必须要加@Sharable ,表明它是可以共享的,否则 第二次建立连接的时候会报错:
io.netty.channel.ChannelPipelineException: xxxHandler is not a @Sharable handler, so can‘t be added or removed multiple times.
这样做的目的 大概是 以防 使用方 忘记了 实例是可以共享的, 需要他创建自定义Handler 的时候就引起注意。
源码分析
首先 DefaultChannelPipeline所有的addXxx 方法, 有调用checkMultiplicity,从而保证了逻辑一致。如:
@Override public final ChannelPipeline addFirst(EventExecutorGroup group, String name, ChannelHandler handler) { final AbstractChannelHandlerContext newCtx; synchronized (this) { checkMultiplicity(handler); ...
然后,在DefaultChannelPipeline#checkMultiplicity:
private static void checkMultiplicity(ChannelHandler 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; } }
因为checkMultiplicity方法每次 添加连接都会执行,那么第一次执行完之后 h.added 就是 true, 后面如何在添加连接, 那么还会执行到这里,那么 如果是共享的 h , 也就是如果一个实例, 同一个h, 那么它的 added 字段值就还是 true,那么 就需要判断 h.isSharable() ,h.isSharable() == true 意味着可以共享,可以共享才可以添加。 否则就不让添加。
这是一个强制的做法。 就是强制如果需要共享, 就必须添加 @Sharable 注解。
这样做的目的 大概是 以防 使用方 忘记了 实例是可以共享的, 需要他创建自定义Handler 的时候就引起注意。
不同Handler需要共享信息的时候, 干脆就使用一个Handler,而不是多个。
对于一般的TCP ,其实现在io.netty.channel.ChannelHandlerAdapter#isSharable 方法。
/** * Return {@code true} if the implementation is {@link Sharable} and so can be added * to different {@link ChannelPipeline}s. */ public boolean isSharable() { /** * Cache the result of {@link Sharable} annotation detection to workaround a condition. We use a * {@link ThreadLocal} and {@link WeakHashMap} to eliminate the volatile write/reads. Using different * {@link WeakHashMap} instances per {@link Thread} is good enough for us and the number of * {@link Thread}s are quite limited anyway. * * See <a href="https://github.com/netty/netty/issues/2289">#2289</a>. */ Class<?> clazz = getClass(); Map<Class<?>, Boolean> cache = InternalThreadLocalMap.get().handlerSharableCache(); Boolean sharable = cache.get(clazz); if (sharable == null) { sharable = clazz.isAnnotationPresent(Sharable.class); cache.put(clazz, sharable); } return sharable; }
总之,@Sharable注解定义在ChannelHandler接口里面,该注解被使用是在ChannelHandlerAdapter类里面,被sharable注解标记过的实例都会存入当前加载线程的threadlocal里面。
它和什么多线程、 线程安全, 有一定关系。因为 全局唯一实例意味着 多线程的竞争。
所以呢, 这种 存在 @Sharable 注解、自定义的、全局唯一的实例, 其内部属性最好也要做成线程安全的,否则可能有线程问题。
代码示例
/**
* netty 接收 次数 计数器
*/
@ChannelHandler.Sharable// 表示它可以被多个channel安全地共享
public class ShareableEchoServerHandler extends ChannelInboundHandlerAdapter {
private AtomicInteger integer = new AtomicInteger(0);
private long integeer = 0L;// 不能这样写。。
public ShareableEchoServerHandler() {
System.out.println(this.getClass()
.getSimpleName() + " init....");
}
// 从channel中读取消息时
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
System.out.println(integer.incrementAndGet() + " " + ctx.handler());
System.out.println(integeer ++ + " " + ctx.handler()); // 这样做 看似也可以, 但是有多线程安全问题, 即潜在bug
// ctx.pipeline().fireChannelRead(msg ); // 第 388 次的时候: java.lang.*Errorjava.lang.*Errorjava.lang.*Error
ctx.channel().pipeline().fireChannelRead(msg);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx,
Throwable cause) {
cause.printStackTrace();
ctx.close(); // 关闭该channel
}
}
看官方做法示例:
可见,其中 ConcurrentMap、AtomicLong、 volatile 保证了线程安全; 其中maxGlobalWriteSize 是volatile ,只需要保证 可见性即可。why ? 因为它的值不是累加的,后面的值不依赖于之前的旧值。
使用场景
使用的场景就是 适用单例的地方,
为什么要把handler作为单例使用? 1.方便统计一些信息,如连接数 2.方便再所有channel值间共享以下而信息 。。
明白了 单例, 也就明白了它。
通常的使用场景:
1 handler中的计数服务,需要一直在递增。
2 使用这种线程共享的handler可以避免频繁创建handler带来的系统开销
3 适用于某些支持线程共享的handler,比如日志服务,计数服务等。
4 适用于没有成员变量的encoder、decoder
5 粘包问题的拆包的时候,需要共享一下中间数据、变量
..
总结
总结一下,它其实就是为了共享的方面,然后为了提升一点性能。
其用法很简单,两个情况:
1 如果每次通过new 而不是共享的方式,那么加不加@Sharable 效果都是一样的。每个Channel使用不通的ChannelHandler 对象。
如:ch.pipeline().addLast(new EchoServerHandler());
2 如果通过共享的方式,也就是 Handler 实例只有一个,那么必须要加@Sharable ,表明它是可以共享的,否则 第二次建立连接的时候会报错:
io.netty.channel.ChannelPipelineException: xxxHandler is not a @Sharable handler, so can‘t be added or removed multiple times.
另外,对于存在 @Sharable 注解、自定义的、全局唯一的实例,要注意线程同步的问题,其内部属性最好也要做成线程安全的,否则可能有线程问题。
参考:
https://www.cnblogs.com/technologykai/articles/10907567.html
https://blog.csdn.net/weixin_35891744/article/details/114742844?utm_medium=distribute.pc_relevant.none-task-blog-baidujs_title-4&spm=1001.2101.3001.4242