netty @Sharable 注解详解

Posted 10年 Java程序员,硬核人生!勇往直前,永不退缩!

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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
 

以上是关于netty @Sharable 注解详解的主要内容,如果未能解决你的问题,请参考以下文章

@PostConstruct和@PreDestroy注解

判断class类是否有对应注解

[年薪60W分水岭]基于Netty手写实现Apache Dubbo(带注册中心和注解)

Java线程池详解

netty服务端Chanel注册与线程模型详解(附带源码分析)

Netty实战入门详解——让你彻底记住什么是Netty(看不懂你来找我)