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!
如果您觉得阅读本文对您有帮助,请点一下“推荐”按钮,您的“推荐”将是我最大的写作动力!欢迎各位转载,但是未经作者本人同意,转载文章之后必须在文章页面明显位置给出作者和原文连接,否则保留追究法律责任的权利。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)