• netty @Sharable 注解详解


    注解说明

     
    @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.StackOverflowErrorjava.lang.StackOverflowErrorjava.lang.StackOverflowError
    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
     


    版权声明
    本文原创发表于 博客园,作者为 阿K .     本文欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则视为侵权。
    欢迎关注本人微信公众号:觉醒的码农,或者扫码进群:

  • 相关阅读:
    数据库常用连接字符串(网址)
    WPF布局(2)控件拖动
    TreeView templete(code project)
    寄宿(host)和应用程序域(appdomain)
    C#DSN操作
    WPF布局(3)坐标(转)
    C#注册表操作
    程序集加载与反射
    关于RichTextBox 及 RTF格式文件的保存
    Ajax实现不刷屏的前提下实现页面定时刷新
  • 原文地址:https://www.cnblogs.com/FlyAway2013/p/14882319.html
Copyright © 2020-2023  润新知