1.7 分段锁
1.8 CAS + volatile + synchronized
初始化
private final Node<K,V>[] initTable() { Node<K,V>[] tab; int sc; while ((tab = table) == null || tab.length == 0) { //sizeCtl为-1时临时充当标记位,表明有线程在执行初始化了 if ((sc = sizeCtl) < 0) Thread.yield(); // lost initialization race; just spin //long offset, int expected, int x // 设置sizeCtl,SIZECTL是sizeCtl在对象内存中的偏移量 else if (U.compareAndSetInt(this, SIZECTL, sc, -1)) { try { if ((tab = table) == null || tab.length == 0) { int n = (sc > 0) ? sc : DEFAULT_CAPACITY; @SuppressWarnings("unchecked") Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n]; table = tab = nt; sc = n - (n >>> 2); } } finally { sizeCtl = sc; } break; } } return tab; }
总的流程是,循环CAS设定标记位,只有设定成功的线程才能进行初始化,其他线程自旋等待
put
- 第一次put时先进行初始化过程
- 不允许key, value为null,抛异常
- CopyOnWrite:每次重新取表的引用,表是volatile
- 计算hashcode, 插入位置,判定冲突
- 未冲突:循环CAS设定值直至成功,返回
- 冲突:锁分离,锁住链表头对象再插入,提高并发程度
- 扩容:协作扩容,扩容完毕再put
扩容
多线程并发扩容:
- 当表长达到容量,再put时进行扩容
- rehash时可以依据hash值在表的长度那一位的值是0还是1将节点分为两类
- 比如说表长为8,一个节点的hash值为1,1111,1111,另一个为0,0110,1111,第二个会被放到新表中同一个索引位上,而第一个则会放到原始索引位+原始表长的位置上去
- 锁住头节点,然后分类批量进行迁移
协作扩容:
- 如果put时发现头节点hash值位负数,表示正进行扩容,这时put的线程就参与进行一起扩容,完成后再进行Put
扩容时get:
- volatile包装拿到最新的节点数据,如果get时发现已迁移,则在新表中get