Netty之DefaultAttributeMap與AttributeKey的機制和原理

延年有餘發表於2022-03-25

一、介紹和原理分析

1.什麼是 DefaultAttributeMap?

DefaultAttributeMap 是一個 陣列 + 連結串列 結構的執行緒安全Map

2.什麼是 AttributeKey?

AttributeKey可以想象成一個快取set,存放了一組key的集合,與DefaultAttributeMap之間的關係是,後者中的雜湊圖存放鍵值對(k-v)的v即是AttributeKey

有了AttributeKey,你自然會想到Attribute,兩者之間又有什麼關係呢?下面會講,慢慢理解,跟著我思路!

3. 什麼是 Attribute?

Attribute顧名思義,就是與AttributeKey是一對的,形象一點說就是你跟你的物件(老婆),而你就是key,是一對一的,不能是一對多的關係

憑什麼是一對一,也就是憑什麼你只能有一個物件?
AttributeKey它受DefaultAttributeMap中的內部類DefaultAttribute約束,前面說了DefaultAttributeMap的結構是以陣列和連結串列的形式,其實它的最小單元(結點)就是DefaultAttribute

4. 關於陣列和連結串列的結構

  • 陣列採用的是 AtomicReferenceArray , 連結串列 中 節點為 DefaultAttribute 結構;
  • DefaultAttribute 繼承了 AtomicReference,所以也是具有與AtomicReference相同的原子操作;
  • 陣列和連結串列都是執行緒安全的;

5. DefaultAttributeMap 與 AtomicReferenceArray 的關係圖

其中,每個結點DefaultAttribute的欄位就沒有詳細畫出來

graph LR subgraph DefaultAttributeMap subgraph "AtomicReferenceArray(陣列)" end end

陣列預設建立大小為4,如下圖所示

graph LR subgraph "AtomicReferenceArray" subgraph "下標1" head1(head) end subgraph "下標2" head2(head) end subgraph "下標3" head3(head) end subgraph "下標4" head4(head) end head1 --> next1(next) head2 --> next2(next) head3 --> next3(next) head4 --> next4(next) next1--> next1-1(next) next2--> next2-1(next) next3--> next3-1(next) next4--> next4-1(next) end

6. valueOf("key")原理

預設情況下,第一次存放key值時,一般使用 AttributeKey.valueOf("rpcResponse"),此時在AttributeKey中的常量池會隨之建立,並初始化好ConcurrentHashMap,下面通過原始碼追蹤

使用AttributeKey的靜態方法valueOf("key")

public final class AttributeKey<T> extends AbstractConstant<AttributeKey<T>> {
    // static final 修飾的 引用型別在 類初始化階段 就已經完成
    //簡單使用AttributeKey不會觸發類初始化,訪問了靜態方法valueOf()導致了初始化
    private static final ConstantPool<AttributeKey<Object>> pool = new ConstantPool<AttributeKey<Object>>() {
}

pool 已被例項化,類中的屬性也會例項化

public abstract class ConstantPool<T extends Constant<T>> {
    private final ConcurrentMap<String, T> constants = PlatformDependent.newConcurrentHashMap();
    private final AtomicInteger nextId = new AtomicInteger(1);

.valueOf("rpcResponse")該方法呼叫後,會先去new一個AbstractConstant物件,優先對它的id值和name值(傳進的key)進行初始化

public class ChannelOption<T> extends AbstractConstant<ChannelOption<T>> {
	protected ChannelOption<Object> newConstant(int id, String name) {
		return new ChannelOption(id, name);
	}
	// 省略幾行
	private ChannelOption(int id, String name) {
		super(id, name);
	}
}

ConcurrentHashMap中呼叫putIfAbsent方法將key值存入,方法是為空才放入的意思,每次都會返回一個初始化idkey值的AbstractConstant

 private T getOrCreate(String name) {
     T constant = (Constant)this.constants.get(name);
     if (constant == null) {
     // new 完後 返回給 tempConstant 
         T tempConstant = this.newConstant(this.nextId(), name);
         constant = (Constant)this.constants.putIfAbsent(name, tempConstant);
         if (constant == null) {
             return tempConstant;
         }
     }

     return constant;
 }

最後強制轉換成了AttributeKey並返回

public static <T> AttributeKey<T> valueOf(String name) {
    return (AttributeKey)pool.valueOf(name);
}

下次再使用valueOf("")傳入引數時,如果引數相同,會去拿AttributeKey(舊值)返回

講到這裡,那麼在多執行緒環境下,常量池和雜湊表是共享的嗎?

答案當然是肯定的!

那多執行緒環境下只存在一個執行緒池和雜湊表嘛?

答案也是明確的,staic final 修飾的變數,是在類載入階段完成的,虛擬機器會保證執行緒安全

7. newInstance 原理

newInstancevalueOf 的 原理 異常類似,都是樂觀鎖的思想,只是 在多執行緒環境下前者要 丟擲 異常(不太準確,後面總結會糾正),後者直接返回同一個

public T newInstance(String name) {
    checkNotNullAndNotEmpty(name);
    return this.createOrThrow(name);
}

newInstance 呼叫的方法是 常量池中的 createOrThrow,而 valueOf 呼叫的方法是 getOrCreate

private T createOrThrow(String name) {
    T constant = (Constant)this.constants.get(name);
    // putIfAbsent 方法執行完畢後,其他執行緒將會直接丟擲異常
    if (constant == null) {
        T tempConstant = this.newConstant(this.nextId(), name);
        // 多執行緒環境下,多個執行緒能夠進入這裡
        constant = (Constant)this.constants.putIfAbsent(name, tempConstant);
        // 不過 在 後執行 putIfAbsent 的執行緒,會先 阻塞在該方法中的 sychronized 同步程式碼塊中
        // 也有 先 返回的 執行緒,return null,會去直接拿到 tempConstant,與 return 的地址 是					       
        //同一個
        if (constant == null) {
            return tempConstant;
        }
    }
	
    throw new IllegalArgumentException(String.format("'%s' is already in use", name));
}

8. ctx.channel().attr(key).set(T object)與 get() 原理:

首先是先操作ctx.channel().attr(key),返回的值型別為Attribute,使用的attr方法,是因為Channel繼承了AttributeMap,呼叫的方法實際上是對實現類DefaultAttributeMap中實現方法的呼叫

原始碼雖然篇幅有點長,但其實不難理解,原始碼用的版本是netty-all-4.1.20.Final

public <T> Attribute<T> attr(AttributeKey<T> key) {
    if (key == null) {
        throw new NullPointerException("key");
    } else {
        AtomicReferenceArray<DefaultAttributeMap.DefaultAttribute<?>> attributes = this.attributes;
        if (attributes == null) {
            attributes = new AtomicReferenceArray(4);
            if (!updater.compareAndSet(this, (Object)null, attributes)) {
                attributes = this.attributes;
            }
        }
		/** index 是 取出 key 的 id 值 與 3 與 運算,3是因為建立陣列預設就是3
		*   這裡由於 key 的 id 值 是 加1 增長的,所以 每次 都是 類似於 雜湊演算法的 
		*   %3 來命中槽位
		*/  
        int i = index(key);
        DefaultAttributeMap.DefaultAttribute<?> head = (DefaultAttributeMap.DefaultAttribute)attributes.get(i);
        //該 下標 未使用,也就是 還沒有頭結點,需先 初始化 頭結點
        if (head == null) {
        	// 頭結點不會 存入 key 值
            head = new DefaultAttributeMap.DefaultAttribute();
            // key 值 存入到 了 欄位 key 中,見下一個程式碼段
            DefaultAttributeMap.DefaultAttribute<T> attr = new DefaultAttributeMap.DefaultAttribute(head, key);
            head.next = attr;
            attr.prev = head;
            if (attributes.compareAndSet(i, (Object)null, head)) {
                return attr;
            }

            head = (DefaultAttributeMap.DefaultAttribute)attributes.get(i);
        }
		// 這裡要做 執行緒安全,因為只有原子操作是執行緒安全,但原子組合操作就不是執行緒安全的了
        synchronized(head) {
            DefaultAttributeMap.DefaultAttribute curr = head;
			/**
			*	直到找到 key 值 相同 的結點,否則 遍歷到 尾結點,沒有找到則 
			* 	通過 尾插入 新節點 再將其返回			
			*/
            while(true) {
                DefaultAttributeMap.DefaultAttribute<?> next = curr.next;
                if (next == null) {
                    DefaultAttributeMap.DefaultAttribute<T> attr = new DefaultAttributeMap.DefaultAttribute(head, key);
                    curr.next = attr;
                    attr.prev = curr;
                    return attr;
                }

                if (next.key == key && !next.removed) {
                    return next;
                }

                curr = next;
            }
        }
    }
}

一個有效結點只跟一個AttributeKey繫結,不包括head頭結點,下面引數2作為了key值傳入建構函式,接著返回型別為DefaultAttribute的結點

DefaultAttribute(DefaultAttributeMap.DefaultAttribute<?> head, AttributeKey<T> key) {
    this.head = head;
    this.key = key;
}

返回的結點型別就是前面說的Attribute,但該結點沒有value屬性,又是怎麼存進去的呢?對set()方法通過原始碼追蹤

其實該節點DefaultAttribute繼承了AtomicReference

private static final class DefaultAttribute<T> extends AtomicReference<T> implements Attribute<T> {
}

使得結點多了一個value欄位,形象來說,就是你已經跟你物件結合在了一起,一個節點的key對應著一個value了,都在同一個DefaultAttribute類中

public class AtomicReference<V> implements java.io.Serializable {
    private static final VarHandle VALUE;
}

get() 原理 與 set()方法一樣,不再贅述

二、總結

1. valueOf

可以看出最關鍵的方法是 getOrCreate,這個方法最大的特點是採用類樂觀鎖的方式,當我們最後發現了 constant != null時,那麼我們返回已經插入的 constant

2. newInstance

可以看出最關鍵的方法是 createOrThrow,這個方法最大的特點是採用類樂觀鎖的方式,當我們最後發現了 constant != null時,我們直接丟擲異常。

3. valueOf和newInstance 對比

valueOf:如果 namenull、空字串時丟擲異常,不存在就建立一個,且多執行緒隨先建立返回誰。
newInstance : 如果namenull、空字串或存在時,就丟擲異常,且多執行緒建立,第一個成功建立後,其他能判斷到第一個if裡面的的幾個執行緒返回建立值,其他執行緒丟擲異常。

借鑑:
簡書:https://www.jianshu.com/p/e7d9a2e8c0ac
官方文件:https://netty.io/4.1/api/index.html

三、結束語

評論區可留言,可私信,可互相交流學習,共同進步,歡迎各位給出意見或評價,本人致力於做到優質文章,希望能有幸拜讀各位的建議!
與51cto同步:https://blog.51cto.com/fyphome
與csdn同步:https://blog.csdn.net/F15217283411

專注品質,熱愛生活。
交流技術,尋求同志。
—— 延年有餘 QQ:1160886967

相關文章