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 注解、自定义的、全局唯一的实例,要注意线程同步的问题,其内部属性最好也要做成线程安全的,否则可能有线程问题。
 

  • 16
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值