Collection - Map & List & Set

1.7-hashtable = 数组 + 链表

(>=) 1.8 = 数组 + 链表 + 红黑树

HashMap 的容量 -》 数组的大小

new HashMap(): 如果不写构造参数,默认大小是16,

如果说写了初始容量:11,hashmap的初始容量就是11?

Hash冲突解决方式:

1.7 头插法

HashMap并不是用取模计算index,而是用位运算!

效率:位运算 > %

并没有说HashMap的容量一定是16,

/** The default initial capacity - MUST be a power of two. */ 必须是2的指数幂?

roundUpToPowerOf2(size);   // 强行将非2的指数次幂的数值转化成2的指数次幂

怎么转化?

1. 必须最接近size,11

2. 必须大于等于size 

3. 必须是2的指数次幂

为什么一定要转成2的指数次幂?

计算索引:int i = indexOf(hash, table.length);

static int indexFor(int h, int length) {

  return h & (table.lenght - 1);

}

HashMap扩容

当前HashMap存了多少Element,size >= threshold,threshold为扩容阈值

threshold扩容阈值 = capacity * 加载因子。

扩容怎么扩?

扩容为原来的2倍,

转移数据,

 1 void transfer(Entry[] newTable, boolean rehash){
 2     int newCapacity = newTable.length;
 3     for(Entry<K,V> e : table){
 4         while(null != e){
 5             Entry<k,v> next = e.next;
 6             if(rehash){
 7                 e.hash = null == e.key ? 0 : hash(e.key);
 8             }
 9             int i = indexFor(e.hash, newCapacity);
10             e.next = newTable[i];
11             e = next;
12         }
13     }
14 }

1.7 :在高并发情况下,由于上面的代码,可能会造成链表成环,可以用两个线程同时进入该方法进行扩容,其中一个线程卡在了第5步,就可能造成环状链表。如果有新的元素想往这个HashMap放入元素时,由于put方法有一个循环判断是否链表中有重复元素,在循环时,一直next != null,这样就会造成刚刚说的链表成环,死锁问题。

hash扩容,有个加载因子?loadFactor = 0.75,为什么是0.75?

空间和时间上的均衡。如果是0.5,可能会造成不断扩容,导致浪费很多空间。如果是1,可能会造成很多的hash碰撞,不过是put还是get操作,都可能会因为经常发生hash碰撞,而使得时间复杂度编程O(n)。

其实最优的loadFactor根据牛顿二项式求极限,其最佳值为0.68。

 

1.8及之后引入红黑树。

Hash表容量 >= 64 才会链表转为红黑树,否则优先扩容。

只有等链表过长,阈值设置TREEIFY_THRESHOLD=8,也就是说只有链表长度 > 8的时候才会转为红黑树。

在1.8中对hash表扩容做了优化,把1.7存在的链表成环、死锁问题解决了。

其采用了高低位指针实现扩容。

可以看到jdk1.8之后,扩容不会再次进行rehash,但是要满足高低位移动,数组容量必须是2的幂次方。

可以巧妙地利用jdk1.8扩容这里的思想,对二进制数据的巧妙运用,用于分库分表,在线扩容。

do {
    next = e.next;
    if ((e.hash & oldCap) == 0) {
        if (loTail == null)
            loHead = e;
        else
            loTail.next = e;
        loTail = e;
    }
    else {
        if (hiTail == null)
            hiHead = e;
        else
            hiTail.next = e;
        hiTail = e;
    }
} while ((e = next) != null);
if (loTail != null) {
    loTail.next = null;
    newTab[j] = loHead;        // 低位移动到新的数组上的同样的index位置
}
if (hiTail != null) {
    hiTail.next = null;
    newTab[j + oldCap] = hiHead;  // 如果是高位,则移到新数组的index = index + oldCap
}

 

重要成员变量

DEFAULT_INITIAL_CAPACITY = 1<<4; Hash表默认初始容量

MAXIMUM_CAPACITY= 1<<30; 最大Hash表容量

DEFAULT_LOAD_FACTOR=0.75f; 默认加载因子

TREEIFY_THRESHOLD=8; 链表转红黑树阈值

UNTREEIFY_THRESHOLD=6; 红黑树转链表阈值

MIN_TREEIFY_CAPACITY=64; 链表转红黑树时hash表最小容量阈值,达不到优先扩容。

ConcurrentHashMap

写同步,读无锁

 jdk1.7

ConcurrentHashMap , 分段锁!减少竞争的冲突。

 

重要成员变量

ConcurrentHashMap拥有出色的性能,在真正掌握内部结构时,先要掌握比较重要的成员:

  • LOAD_FACTOR: 负载因子,默认75%,当table使用率达到75%时,为减少table的hash容量,table长度将扩容一倍,负载因子计算:元素总个数 % table.length
  • TREEIFY_THRESHOLD: 默认8, 当链表长度达到8时, 将结构转变为红黑树。 
  • UNTREEIFY_THRESHOLD: 默认6, 红黑树转变为链表的阈值。 
  • MIN_TRANSFER_STRIDE: 默认16, table扩容时, 每个线程最少迁移table的槽位 个数。 
  • MOVED: 值为-1, 当Node.hash为MOVED时, 代表着table正在扩容 
  • TREEBIN, 置为-2, 代表此元素后接红黑树。 
  • nextTable: table迁移过程临时变量, 在迁移过程中将元素全部迁移到nextTable 上。 
  • sizeCtl: 用来标志table初始化和扩容的,不同的取值代表着不同的含义:
    • 0: table还没有被初始化

    • -1: table正在初始化

    • 小于-1: 实际值为resizeStamp(n) <<RESIZE_STAMP_SHIFT+2, 表明table正在扩容
    • 大于0: 初始化完成后, 代表table最大存放元素的个数, 默认为0.75*n

HashTable,全局锁

jdk1.8:

ConcurrentHashMap:

1. 初始化Hash表

final V putVal(K key, V value, boolean onlyIfAbsent) {
     if (key == null || value == null) throw new NullPointerException();
     //获取hash值
     int hash = spread(key.hashCode());
     int binCount = 0;
     for (Node<K,V>[] tab = table;;) {
         Node<K,V> f; int n, i, fh;
         //如果table为空
         if (tab == null || (n = tab.length) == 0)
             //进行初始化
             tab = initTable();
         //如果table已经初始化,下标为(n - 1) & hash,相当于hash % n,但是对应的槽位还未初始化
         else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
             //初始化Node,并将Node设置为tab[i],设置成功就退出 
             if (casTabAt(tab, i, null,
                          new Node<K,V>(hash, key, value, null)))
                 break;                 
         }
         //如果f.hash = MOVED,说明当前槽位正在扩容,先帮助扩容,扩容完毕后再插入
         else if ((fh = f.hash) == MOVED)
             //帮助扩容
             tab = helpTransfer(tab, f);
         else {
             V oldVal = null;
             //用槽的第一个Node作为锁对象
             synchronized (f) {
                 if (tabAt(tab, i) == f) {
                     if (fh >= 0) {//红黑树有一个哨兵节点,hash值为-2,如果fh>0说明是链表
                         binCount = 1;//binCount代表着链表中的个数
                         for (Node<K,V> e = f;; ++binCount) {
                             K ek;
                             //如果存在对应的key
                             if (e.hash == hash &&
                                 ((ek = e.key) == key ||
                                  (ek != null && key.equals(ek)))) {
                                 oldVal = e.val;
                                 if (!onlyIfAbsent)
                                     e.val = value;
                                 break;
                             }
                             Node<K,V> pred = e;
                             //到了链表末尾
                             if ((e = e.next) == null) {
                                 pred.next = new Node<K,V>(hash, key,
                                                           value, null);
                                 break;
                             }
                         }
                     }
                     //如果是红黑树
                     else if (f instanceof TreeBin) {
                         Node<K,V> p;
                         binCount = 2;
                         //调用红黑树的put方法
                         if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                        value)) != null) {
                             oldVal = p.val;
                             if (!onlyIfAbsent)
                                 p.val = value;
                         }
                     }
                 }
             }
             
             if (binCount != 0) {
                 //如果超过树化阈值,升级为红黑树
                 if (binCount >= TREEIFY_THRESHOLD)
                     treeifyBin(tab, i);
                 if (oldVal != null)
                     return oldVal;
                 break;
             }
         }
     }
     //将size++,如果超过阈值,会触发扩容
     addCount(1L, binCount);
     return null;
 }

 下面是协助扩容的过程:

final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) { //table扩容
        Node<K,V>[] nextTab; int sc;
        if (tab != null && (f instanceof ForwardingNode) &&
            (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
            // 根据 length 得到一个标识符号
            int rs = resizeStamp(tab.length);
            while (nextTab == nextTable && table == tab &&
                   (sc = sizeCtl) < 0) {//说明还在扩容
                //判断是否标志发生了变化||  扩容结束了
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                     //达到最大的帮助线程 ||  判断扩容转移下标是否在调整(扩容结束)
                    sc == rs + MAX_RESIZERS || transferIndex <= 0)
                    break;
                // 将 sizeCtl + 1, (表示增加了一个线程帮助其扩容)
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
                    transfer(tab, nextTab);
                    break;
                }
            }
            return nextTab;
        }
        return table;
}

参考:https://blog.csdn.net/qq_40276626/article/details/119972653

CopyOnWriteArrayList

CopyOnWrite机制

核心思想:读写分离,空间换时间,避免为保证并发安全导致的激烈的锁竞争。

 ConcurrentSkipListMap

并发场景下能保证key有序的一种map结构。

跳表-时间复杂度(O(logn)),保证map的key的顺序,底层的数据结构基于链表,跳表维护索引,根据索引去找。

End!

 

posted @ 2022-11-08 06:59  君莫笑我十年游  阅读(16)  评论(0编辑  收藏  举报