HashMap && HashTable && synchronizedMap&& ConcurrentHashMap 面试常问题

HashMap && HashTable && synchronizedMap&& ConcurrentHashMap 面试常问题

开始

Collections.synchronizedMap()

  1. HashMap是线程不安全的,那么怎么解决线程不安全呢?

    • 使用HashTable 代替
    • 使用Collections.synchronizedMap() 代替
    • 使用ConcurrentHashMap 代替

    但是,最好用ConcurrentHashMap。

  2. Collections.synchronizedMap() 怎么解决线程不安全的?

    它的源码内部有一个final修饰的Map,而且还有一个独占对象锁。通过构造参数接收传过来的map给到自己的final map。然后给所有公共方法加锁,锁的是代码块。SynchronizedMap是Collections的私有内部静态类。

    private static class SynchronizedMap<K,V>
            implements Map<K,V>, Serializable {
            private static final long serialVersionUID = 1978198479659022715L;
    
            private final Map<K,V> m;     // Backing Map
            final Object      mutex;        // Object on which to synchronize
    
            SynchronizedMap(Map<K,V> m) {
                this.m = Objects.requireNonNull(m);
                mutex = this;
            }
    
            SynchronizedMap(Map<K,V> m, Object mutex) {
                this.m = m;
                this.mutex = mutex;
            }
    

    可见一个final 修饰的 Map 和一个final修饰的独占锁mutex

    一部分公共方法

    public int size() {
        synchronized (mutex) {return m.size();}
    }
    public boolean isEmpty() {
        synchronized (mutex) {return m.isEmpty();}
    }
    public boolean containsKey(Object key) {
        synchronized (mutex) {return m.containsKey(key);}
    }
    public boolean containsValue(Object value) {
        synchronized (mutex) {return m.containsValue(value);}
    }
    public V get(Object key) {
        synchronized (mutex) {return m.get(key);}
    }
    
    public V put(K key, V value) {
        synchronized (mutex) {return m.put(key, value);}
    }
    public V remove(Object key) {
        synchronized (mutex) {return m.remove(key);}
    }
    public void putAll(Map<? extends K, ? extends V> map) {
        synchronized (mutex) {m.putAll(map);}
    }
    public void clear() {
        synchronized (mutex) {m.clear();}
    }
    

    可见,都是被synchronized修饰的代码块。

HashTable

  1. HashTable是什么?

    HashTable是可以用与多线程场景下,并发安全的类。和HashMap类似,区别就是HashTable是线程安全的,而HashMap不是。但是HashTable性能差。

  2. 为什么性能差?

    因为HashTable会将所有公共方法都加上锁,对方法加锁。就连get方法都是直接加锁

    public synchronized V get(Object key) {
        Entry<?,?> tab[] = table;
    
  3. HashTable和HashMap还有什么差别?

    1. HashTable的key和value不能用null值,而HashMap可以。

      为什么?

      HashTable对于key为null 直接抛出空指针异常。而HashMap则是进行了处理。看下put函数

      HashTable

      public synchronized V put(K key, V value) {
              // Make sure the value is not null
              if (value == null) {
                  throw new NullPointerException();
              }
           int hash = key.hashCode();//这里没有对key为null处理,如果是key==null,则抛空指针异常
      

      HashMap

      //并没有判断value为null的代码。对于key的处理则是如果为null,返回hashcode为0
      static final int hash(Object key) {
              int h;
              return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
          }
      

      这是代码层次的处理,具体为什么是指?

      HashTable使用的是安全失败的机制,fail-safe。在这个机制下不能保证每次读取的值都是最新值。如果使用null,就会使其无法判断是不存在还是为null。因为无法调用一次contain(key)来对key是否存在判断,ConcurentHashMap同理。

      ConcurrentHashmap和Hashtable都是支持并发的,这样会有一个问题,当你通过get(k)获取对应的value时,如果获取到的是null时,你无法判断,它是put(k,v)的时候value为null,还是这个key从来没有做过映射。HashMap是非并发的,可以通过contains(key)来做这个判断。而支持并发的Map在调用m.contains(key)和m.get(key),m可能已经不同了,m为定位到的Segment,每次put值都可能会定位到不同的段。

      fail-safe 是什么?

      为了实现并发修改,这些支持并发的类都使用fail-safe。这些类在遍历的时候并不是访问原有集合,而是重新复制一份当前的数据。在此数据上遍历。这样其它线程修改原有数据就不会影响到遍历了。也有明显的缺点,就是不能遍历时知道遍历过程中原集合新的修改。

    2. 实现方式不同

      Hashtable 继承了Dictionary类,而HashMap则是AbstractMap

    3. 初始化化容量不同

      Hashtable,初始容量 11 而HashMap 16 默认负载因子都是0.75

    4. 扩容机制不同

      HashMap 是 当前容量翻倍,而HashTable是当前容量翻倍+1

    5. 迭代器不同

      HashMap迭代器有fail-fast,而Hashtable无

      fail-fast,是java.util包下集合类都有的一个性质。主要用与多线程迭代器遍历下,能够快速失败,并不是在未来某个不确认的时间失败。如果迭代器遍历时,集合元素有更新,即新增、删除、修改。都会使迭代器抛出ConcurrentModificationException

      实现:在迭代器遍历时,会将当前modCount记录到expectedmodCount。modCount是一个int形变量transient int modCount;int expectedModCount;

      而每当有新增,修改,删除集合元素时都会时modCount改变。此时就和expectedmodCount不相等,即抛异常

ConcurrentHashMap

1.7版本

实现就和HashMap1.7差不多。就是加了分段Segment ,然后这个Segment里面有一部分HashEntry(这个可以说是一部分的数组) 也就是说一个段就相当于截取了一部分桶,然后加锁。

static final class Segment<K,V> extends ReentrantLock implements Serializable {

    private static final long serialVersionUID = 2249069246763182397L;

    // 和 HashMap 中的 HashEntry 作用一样,真正存放数据的桶
    transient volatile HashEntry<K,V>[] table;

    transient int count;
        // (fail—fast)
    transient int modCount;
        // 大小
    transient int threshold;
        // 负载因子
    final float loadFactor;

}

可见Segment继承了ReentrantLock。HashEntry 和HashMap中的差不多,重点是用了volatile修饰,修饰了它的value和下一个节点next;

volatile的特性是啥?

  1. 保证可见性,对volatile读来说,其它线程对该变量的修改都可见。或者说volatile写,那么此变量的变化将对其它所有线程可见。

  2. 禁止指令重排序,有序性

  3. 单个变量原子性。

  4. 此版本的ConcurrentHashMap怎么实现的并发安全?为什么并发度高?

    由上面可知,Segment可以认为是原数组的一个段而且继承了ReentranLock即线程访问此段会加锁。而一个ConcurrentHashMap会有很多个Segment(默认16) 即可以让16个线程同时对这16个段put元素。所有这就是分段锁。每一个段和每一个都互不影响,各自加锁。

    public V put(K key, V value) {
        Segment<K,V> s;
        if (value == null)
            throw new NullPointerException();//这就是为啥他不可以put null值的原因
        int hash = hash(key);
        int j = (hash >>> segmentShift) & segmentMask;
        if ((s = (Segment<K,V>)UNSAFE.getObject          
             (segments, (j << SSHIFT) + SBASE)) == null) 
            s = ensureSegment(j);
        return s.put(key, hash, value, false);
    }
    

    首先定位到Segment,之后调用此Segment的put方法,可以想象成又是一个小的HashMap。

    final V put(K key, int hash, V value, boolean onlyIfAbsent) {
              // 将当前 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntry
                HashEntry<K,V> node = tryLock() ? null : //尝试获取锁,获取成功就加锁,进入put流程
                    scanAndLockForPut(key, hash, value);
                V oldValue;
                try {
                    HashEntry<K,V>[] tab = table;
                    int index = (tab.length - 1) & hash;
                    HashEntry<K,V> first = entryAt(tab, index);
                    for (HashEntry<K,V> e = first;;) {
                        if (e != null) {
                            K k;
     // 遍历该 HashEntry,如果不为空则判断传入的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value。
                            if ((k = e.key) == key ||
                                (e.hash == hash && key.equals(k))) {
                                oldValue = e.value;
                                if (!onlyIfAbsent) {
                                    e.value = value;
                                    ++modCount;
                                }
                                break;
                            }
                            e = e.next;
                        }
                        else {
                     // 不为空则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判断是否需要扩容。
                            if (node != null)
                                node.setNext(first);
                            else
                                node = new HashEntry<K,V>(hash, key, value, first);
                            int c = count + 1;
                            if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                                rehash(node);
                            else
                                setEntryAt(tab, index, node);
                            ++modCount;
                            count = c;
                            oldValue = null;
                            break;
                        }
                    }
                } finally {
                   //释放锁
                    unlock();
                }
                return oldValue;
            }
    

    首先,第一步是通过tryLock获取锁,获取不到就进入scanAndLockForPut方法

    • 自旋获取锁
    • 自旋次数达到限定值,就阻塞该线程。保证会获取成功

    那么get流程呢?

    key通过hash定位到Segment上,然后再Hash定位到元素。而且HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值。所以,这样是性能高的原因之一,get不加锁。

    但是,随之HashMap改到了1.8,ConcurrentHashMap也改到了1.8.伴随改变,也就是链表长度过长会变红黑树,短了就变链表

1.8版本

数据结构

没有再使用Segment锁了,而是采用synchronized和cas+自旋操作。跟HashMap1.8类似,将HashEntry改成了Node。但是还是用volatile修饰value和next指针。保证可见性,也引入了红黑树

  1. put/get流程

    put流程是在for循环中的,保证新增元素一定成功

       final V putVal(K key, V value, boolean onlyIfAbsent) {
            if (key == null || value == null) throw new NullPointerException();
            int hash = spread(key.hashCode());//对比HashMap 多了一个(h ^ (h >>> 16)) & HASH_BITS 与运算
            int binCount = 0;
            for (Node<K,V>[] tab = table;;) {
                Node<K,V> f; int n, i, fh;
                //判断初始化
                if (tab == null || (n = tab.length) == 0)
                    tab = initTable();
                else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                    //如果计算出的下标位置为null,则cas设置新node,失败就自旋保证成功	
                    if (casTabAt(tab, i, null,
                                 new Node<K,V>(hash, key, value, null)))
                        break;                   // no lock when adding to empty bin
                }
                //判断扩容,需要此线程帮忙迁移元素
                else if ((fh = f.hash) == MOVED)
                    tab = helpTransfer(tab, f);
                else {
                    //这里就是有冲突了,会加锁写入。也就是这里保证了当前槽点只有一个线程可以访问
                    V oldVal = null;
                    synchronized (f) {
                        if (tabAt(tab, i) == f) {
                            if (fh >= 0) {
                                binCount = 1;
                                //这就和HashMap流程差不多了
                                for (Node<K,V> e = f;; ++binCount) {
                                    K ek;
                                    if (e.hash == hash && //判断链表中有没有当前key
                                        ((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) {//无重复key则添加到链表末尾
                                        pred.next = new Node<K,V>(hash, key,
                                                                  value, null);
                                        break;
                                    }
                                }
                            }
                            else if (f instanceof TreeBin) {
                                Node<K,V> p;
                                binCount = 2;
                                if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                               value)) != null) {
                                    oldVal = p.val;
                                    if (!onlyIfAbsent)
                                        p.val = value;
                                }
                            }
                        }
                    }
                    //binCount说明已经添加了
                    if (binCount != 0) {
                        if (binCount >= TREEIFY_THRESHOLD)//判断需要转红黑树吗	
                            treeifyBin(tab, i);
                        if (oldVal != null)
                            return oldVal;
                        break;
                    }
                }
            }
           //继承容器是不是要扩容,如果需要去扩容,调用 transfer 方法去扩容
           // 如果已经在扩容中了,check有无完成
            addCount(1L, binCount);
            return null;
        }
    

    流程:

     1. 进入for循环,判断有无初始化,未初始化则初始化之后继续进入for
     2. 判断槽点是不是空,空的自旋cas设置node。再判断下是不是需要扩容
     3. 有冲突,则加锁,锁槽点。遍历链表看有没有重复key,有则覆盖value。无则加到链表尾部
     4. 看要不要转红黑树。
       	1. 看要不要扩容,并给整个table元素+1
    

    CAS操作是什么?自旋又是什么?

    CAS是乐观锁的一种实现,乐观锁就是认为并发问题不会多,只是在更新数据的时候,用cas判断一下。怎么判断呢?就是cas有一个当前值和内存预期值,比较这俩个值,如果没有修改,则说明在读取到这里比较这段时间里并没有其它线程修改。则直接将新的值赋值。自旋呢就是在cas失败的情况下保证可以把值加到集合,就是再来一遍流程

    cas不能一定保证数据没有被其它线程修改过?ABA问题

    解决可以加上版本号。

    为什么jdk8用synchronized?

    因为java官方修改了synchronized。在1.8之前都是重量级锁。而现在修改为,一开始是偏向锁,即一旦当前线程持有锁之后,就不会再次去获取锁,而是直接比较锁中记录的线程id。如果有竞争,则锁会升级为cas轻量级锁,失败就会短暂自旋,防止被挂起。最好如果以上都失败就换为重量级锁。

  2. get流程

    根据计算的hashcode之后计算的下标,直接在定位桶,如果在桶上就直接返回。是红黑树就按照红黑树寻找。是链表就按照链表。

  3. addCount(1L, binCount);方法用于整个map统计元素个数。并发场景下,统计元素个数也是个难题。可以加volatile,然后cas+1,但是concurrentHashMap有着更优的计数。实现和LongAdder差不多

    首先,是操作一个volatile baseCount 线程cas将值+1,成功返回,失败则将线程分散到不同的CounterCell [i]中,操作计数变量 volatile value +1.

扩容机制
  1. addCount()扩容校验

     private final void addCount(long x, int check) {
            CounterCell[] as; long b, s;
         	//首先会判断当前数组as是否为null,为null说明可能竞争不激烈,就直接使用cas使baseCount+1.
         	//如果cas失败,则进入if语句给当前线程分配一个格子
            if ((as = counterCells) != null ||
                !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
                CounterCell a; long v; int m;
                boolean uncontended = true;//字面意思为无竞争。
                //进入if条件执行 fullAddCount(x, uncontended);有三种可能
                //1.数组as为null
                //2.[ThreadLocalRandom.getProbe() & m 表示计算到的一个格子的下标 如果此格子为null 也进入
                //3. as数组不空,格子不空,那么就cas操作使value+1,失败进入
                if (as == null || (m = as.length - 1) < 0 ||
                    (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
                    !(uncontended =
                      U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
                    fullAddCount(x, uncontended);//确保能够加+1
                    return;
                }
                if (check <= 1)
                    return;
                s = sumCount();
            }
         	//检查扩容,put会默认会检查是不是要扩容
            if (check >= 0) {
                Node<K,V>[] tab, nt; int n, sc;
                //如果s(当前元素个数)>=扩容阈值 && tab!=null  且未达到最大容量 即1<<30
                while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
                       (n = tab.length) < MAXIMUM_CAPACITY) {
                    //当前length的一个标记
                    int rs = resizeStamp(n);
                    //sc小于0代表有线程在扩容
                    if (sc < 0) {	
                        //sc的高16位代表,sc>>>16 然后和rs比较。这俩此时都代表数据校验标识。如果不等于退出循环
                        //sc == rs + 1 ||sc == rs + MAX_RESIZERS 说的是扩容结束了,好像有bug
                        //(nt = nextTable) == null 说明要扩容的数组还未创建 
                        //transferIndex <= 0 说明不需要其它线程帮忙扩容了
                        if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                            sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                            transferIndex <= 0)
                            break;
                        //执行到这里还未break,说明当前线程还能帮助扩容,sc+1.代表扩容的线程数加+1
                        //sc 刚才说了高16位的意思,低16位代表的是正在帮助扩容的线程数
                        if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                            transfer(tab, nt);
                    }
                    //执行到这里,代表sc为扩容阈值,也就是说之前从未有线程执行到这里,也就是说nextTable==null
                    //因为如果有线程执行到这里,会将sc=SIZECTL 置为一个负数。
                    //扩容时,需要用到这个参数校验是否所有线程都全部帮助扩容完成。
                    else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                                 (rs << RESIZE_STAMP_SHIFT) + 2))
                    //扩容,第二个参数代表新表,传入null,则说明是第一次初始化新表(nextTable)
                        transfer(tab, null);
                    s = sumCount();
                }
            }
        }
    

    流程:

    1. 判断当前数组是不是为null,为null代表竞争还不激烈,可以cas操作baseCount+1

    2. 不为null,或者cas失败,代表竞争已开始。如果此时数组还为null或者线程进入的格子为null或者cas格子中的value失败。

      那么就得靠fullAddCount()死循环解决了,这个方法确保能+1

    3. 加+1流程结束,可以开始判断扩容了,首先看有没有到达扩容阈值,且有没有高过上限或低过下限

    4. 可以扩容了,用sc<0来判断当前其它线程是不是正在扩容。正在扩容那么看看要不要本线程帮忙,不要就退出,要就sc+1帮

    5. 到扩容阈值,且无其它线程正在扩容,那么就自己来。自己来怎么让其它线程知道自己扩容,改变sc为负值,其它线程判断sc时就知道了。

  2. transfer()扩容

    先不看代码,真的长,不好理解,先来点概念。

    扩容,只能有一个线程初始化nextTable(扩容后的新表),其它线程都是帮助迁移元素到新的数组的
    在这里插入图片描述

    上图为假设的数组长度为8的数组。所以线程帮助迁移数据的时候都从后往前开始。线程A首先进来迁移桶7和6的数据。而且当前线程迁移会有一个范围,限定能迁移的步长,此时其它线程就不能迁移这部分数据了。且每次固定步长推进,保证不遗漏。线程B进入,发现线程A正在帮忙,就推进2个长度,迁移4,5.为了标识什么时候终止迁移,会维护一个transferIndex,来表示全部线程推移的下标位置。比如说现在线程B到了4,5则transferIndex=4.线程A迁移万6,7则到了2,3同时transferIndex到了2.直到为0表示迁移结束。并且在addCount()那里也会劝退想要参与迁移的线程。

    //这个类是一个标识,用来代表当前桶所以元素以迁移完毕
    static final class ForwardingNode<K,V> extends Node<K,V> {
            final Node<K,V>[] nextTable;
            ForwardingNode(Node<K,V>[] tab) {
                //把当前桶的头结点的 hash 值设置为 -1,表明已经迁移完成,
      			//这个节点中并不存储有效的数据
                super(MOVED, null, null, null);
                this.nextTable = tab;
            }
    
    private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
            int n = tab.length, stride;
        	//确定上面描述的步长,通过cpu数确定,最小是16
            if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
                stride = MIN_TRANSFER_STRIDE; // subdivide range
            //初始化,addCount()保证只有一个线程能进入此if
        	if (nextTab == null) {            // initiating
                try {
                    @SuppressWarnings("unchecked")
                    //数组长度翻倍
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
                    nextTab = nt;
                } catch (Throwable ex) {      // try to cope with OOME
                    sizeCtl = Integer.MAX_VALUE;
                    return;
                }
                //新表
                nextTable = nextTab;
                //推进下标值,迁移完毕为0,此时为元素个数
                transferIndex = n;
            }
            int nextn = nextTab.length;//新数组长度
        	//创建一个标志类,当每次处理完一个桶,就用来表示当前桶已处理完毕
            ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
        	//是否向前推进的标志
            boolean advance = true;
        	//是否所有线程都全部迁移完成的标志
            boolean finishing = false; // to ensure sweep before committing nextTab
        	//i 代表当前线程正在迁移的桶的下标,bound代表它本次可以迁移的范围下限
            for (int i = 0, bound = 0;;) {
                Node<K,V> f; int fh;
                while (advance) {//需要向前推进,transferIndex还没有==0
                    int nextIndex, nextBound;
                    //--i,如果符合条件说明当前已经不需要推进了,结束循环
                    if (--i >= bound || finishing)
                        advance = false;
                    // 已经拷贝完成,满足条件说明无需推进了,都有线程处理了,之后判断线程都处理完毕没
                    else if ((nextIndex = transferIndex) <= 0) {
                        i = -1;
                        advance = false;
                    }
                    //第一个线程会执行这个,cas将nextIndex更新为 transferIndex 的最新值
                    else if (U.compareAndSwapInt
                             (this, TRANSFERINDEX, nextIndex,
                              nextBound = (nextIndex > stride ?
                                           nextIndex - stride : 0))) {
                        bound = nextBound;//初始化步长,就是当前线程可以处理的桶数
                        i = nextIndex - 1;//当前线程在哪个桶
                        advance = false;//还未迁移了,此时在初始化限定值,就不能继续推进了
                    }
                }
                
                if (i < 0 || i >= n || i + n >= nextn) {
                    int sc;
                    //若全部线程都迁移完毕,就是说扩容结束了
                    if (finishing) {
                        nextTable = null;
                        table = nextTab; //设置table为新数组
                        sizeCtl = (n << 1) - (n >>> 1); // 阈值为新数组的0.75
                        return;
                    }
                    //这里,说明此线程已无用,即完成了自己的所有迁移,不管是多少次
                    //SIZECTL 帮助迁移的线程数-1
                    if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                        //这里校验当前sc是不是和初始化新表的那个线程设置的sc相等,相等代表所有线程都迁移完毕了,
                        //也就是说扩容结束了
                        if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                            return;
                        finishing = advance = true;
                        i = n; // recheck before commit
                    }
                }
                //若i的位置元素为空,则说明当前桶的元素已经被迁移完成,就把头结点设置为fwd标志。
                else if ((f = tabAt(tab, i)) == null)
                    advance = casTabAt(tab, i, null, fwd);
                //若当前桶的头结点是 ForwardingNode ,说明迁移完成,则向前推进 
                else if ((fh = f.hash) == MOVED)
                    advance = true; // already processed
                else {
                    //处理当前桶的数据迁移。
                    synchronized (f) {
                        //头结点加锁,只有一个线程可以处理
                        if (tabAt(tab, i) == f) {
                            Node<K,V> ln, hn;
                            if (fh >= 0) {
                                int runBit = fh & n;
                                Node<K,V> lastRun = f;
                                for (Node<K,V> p = f.next; p != null; p = p.next) {
                                    int b = p.hash & n;
                                    if (b != runBit) {
                                        runBit = b;
                                        lastRun = p;
                                    }
                                }
                                if (runBit == 0) {
                                    ln = lastRun;
                                    hn = null;
                                }
                                else {
                                    hn = lastRun;
                                    ln = null;
                                }
                                for (Node<K,V> p = f; p != lastRun; p = p.next) {
                                    int ph = p.hash; K pk = p.key; V pv = p.val;
                                    if ((ph & n) == 0)
                                        ln = new Node<K,V>(ph, pk, pv, ln);
                                    else
                                        hn = new Node<K,V>(ph, pk, pv, hn);
                                }
                                setTabAt(nextTab, i, ln);
                                setTabAt(nextTab, i + n, hn);
                                setTabAt(tab, i, fwd);
                                advance = true;
                            }
                            else if (f instanceof TreeBin) {
                                TreeBin<K,V> t = (TreeBin<K,V>)f;
                                TreeNode<K,V> lo = null, loTail = null;
                                TreeNode<K,V> hi = null, hiTail = null;
                                int lc = 0, hc = 0;
                                for (Node<K,V> e = t.first; e != null; e = e.next) {
                                    int h = e.hash;
                                    TreeNode<K,V> p = new TreeNode<K,V>
                                        (h, e.key, e.val, null, null);
                                    if ((h & n) == 0) {
                                        if ((p.prev = loTail) == null)
                                            lo = p;
                                        else
                                            loTail.next = p;
                                        loTail = p;
                                        ++lc;
                                    }
                                    else {
                                        if ((p.prev = hiTail) == null)
                                            hi = p;
                                        else
                                            hiTail.next = p;
                                        hiTail = p;
                                        ++hc;
                                    }
                                }
                                ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
                                    (hc != 0) ? new TreeBin<K,V>(lo) : t;
                                hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
                                    (lc != 0) ? new TreeBin<K,V>(hi) : t;
                                setTabAt(nextTab, i, ln);
                                setTabAt(nextTab, i + n, hn);
                                setTabAt(tab, i, fwd);
                                advance = true;
                            }
                        }
                    }
                }
            }
        }
    
  3. 协助扩容:helpTransfer(),当线程对当前桶执行put操作时,刚刚好正在扩容。那么插入也执行不了。被置为 forwardingNode。因此线程也不是只等待,而是会帮助扩容

    final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
            Node<K,V>[] nextTab; int sc;
            //头结点为 ForwardingNode ,并且新数组已经初始化
            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;
                    //帮助扩容
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
                        transfer(tab, nextTab);
                        break;
                    }
                }
                return nextTab;
            }
            return table;
        }
    

参考:
【JUC源码】并发容器:ConcurrentHashMap(三)扩容源码分析

我就知道面试官接下来要问我 ConcurrentHashMap 底层原理了
ConcurrentHashMap & Hashtable

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值