来看看ConcurrentHashMap吧

    技术2022-07-13  79

    1. ConcurrentHashMap中没有负载因子和阈值吗 a. 是没有,改用了sizeCtl控制,0表示默认值,-1表示正在扩容,>0表示下一次扩容的门槛,-(1+nThreads)n个线程正在扩容,sizeCtl的变化都是CAS操作 2. 请讲讲ConcurrentHashMap的put操作? a. 控制key和value都不能为null b. 再用自旋+cas实现put过程,下面是具体的put c. 如果桶未初始化就初始化桶 d. 如果桶中还没有元素就把这个元素插进去,插入这一步就是采用的CAS e. 如果要插入的桶正在扩容迁移元素,就当前线程一起帮忙协助扩容 f. 如果桶已经存在且没有迁移元素 g. 就锁住这个桶,是采用分段锁的思想(锁住后里面不需要cas) h. 如果元素存在就替换,不存在size+1,插到链表或者树的尾部 i. 如果桶的元素个数达到了8就尝试树化 j. 元素个数+1(分段锁思想),同时检查是否需要扩容 k. 返回 3. 请讲讲ConcurrentHashMap的扩容操作? 初始化桶操作:判断是否正在扩容,正在的话就让出cpu。否则就用CAS更新sizeCtl(CAS控制只有一个线程初始化桶数组),设置容量,并设置扩容门槛(写死的)赋值给sizeCtl addCount操作,元素数量+1,并会判断是否需要扩容,这里是数组数量采用分段锁的思想,根据不同线程存储到不同段上 先尝试把数量加到baseCount上,如果失败再加到分段的CounterCell上(采用CAS添加) 如果不同线程对应的分段都添加失败,就要扩容段 扩容: sizeCtl存储着扩容门槛,高位存储扩容邮戳,低位存储存储着扩容线程数加1,即(1+nThreads) sc小于0,说明正在扩容,就当前线程加入到迁移元素中去,扩容线程+1; 如果没有线程在扩容,自己就扩容,加入迁移元素,sizeCtl低位为2(1+nThreads) 协助扩容: 线程添加元素时发现正在扩容且当前元素所在的桶元素已经迁移完成了,则协助迁移其它桶的元素。迁移完成的标志:如果桶数组不为空,并且当前桶第一个元素为ForwardingNode类型,并且nextTab不为空 迁移元素: 扩容时容量变为两倍,并把部分元素迁移到其它桶中。迁移元素先从靠后的桶开始;迁移完成的桶在里面放置一ForwardingNode类型的元素,标记该桶迁移完成; 过程: 迁移元素是用sychronized锁住该桶,迁移的时候,根据迁移时根据hash&n是否等于0把桶中元素分化成两个链表或树;等于0的在原来的位置,不等于0 就在原来的位置+n,(n的扩大的大小) 4. 删除流程: a. 删除元素跟添加元素一样,都是先找到元素所在的桶,然后采用分段锁的思想锁住整个桶,再进行操作。 b. 流程:进入自旋,如果正在扩容,协助扩容,如果没有,对桶加锁,找到元素,删除,如果是树退化成链表,还要进行退化操作,删除了元素,就元素个数-1;返回旧值,没有删除元素就返回null 5. 获取元素: a. 不加锁, b. 所以ConcurrentHashMap不是强一致性的‘ 6. Jdk1.7和1.8的区别,底层数据结构有什么不同,put和get操作的不同 a. 数据结构差别:JDK1.7中ConcurrentHashMap采用了数组+Segment+分段锁,segment继承自ReentrantLock,缺点:需要两次hash,hash时间长。优点:并发能力高,segment之间相互不影响;JDK1.8中,数组+链表+红黑树的实现方式来设计,内部大量采用CAS操作 b. 数据结构:取消了Segment分段锁的数据结构,取而代之的是数组+链表+红黑树的结构。 c. 保证线程安全机制:JDK1.7采用segment的分段锁机制实现线程安全,其中segment继承自ReentrantLock。JDK1.8采用CAS+Synchronized保证线程安全。 d. 锁的粒度:原来是对需要进行数据操作的Segment加锁,现调整为对每个数组元素加锁(Node)。 e. 链表转化为红黑树:定位结点的hash算法简化会带来弊端,Hash冲突加剧,因此在链表节点数量大于8时,会将链表转化为红黑树进行存储。 f. 查询时间复杂度:从原来的遍历链表O(n),变成遍历红黑树O(logN)。
    Processed: 0.011, SQL: 9