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
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM