Netty之DefaultAttributeMap与AttributeKey的机制和原理

一、介绍和原理分析

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

posted @ 2022-03-25 13:36  延年有余  阅读(509)  评论(0编辑  收藏  举报