JAVA-HashMap

HashMap的实现原理

基础东西

1)、HashMap的实例有俩个参数影响其性能: “初始容量”(16)装填因子(0.75),也就是超过75%就进行扩容,扩容:newsize = oldsize*2,(扩容的容量为新容量的2倍,所以size一定为2的n次幂)

2)、 解决冲突主要有四种方法:

开放定址法

所谓的开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入

拉链法(链地址法)

基本思想是每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向链表连接起来,若选定的散列表长度为m,则可将散列表定义为一个由m个头指针组成的指针数 组T[0…m-1]。凡是散列地址为i的结点,均插入到以T[i]为头指针的单链表中。T中各分量的初值均应为空指针。在拉链法中,装填因子α可以大于1,但一般均取α≤1。拉链法适合未规定元素的大小

再散列法

选取再hash函数,对于hash值相同的根据hash的值进一步进行rehash

建立公共溢出区

将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表

HashMap是采用拉链法解决哈希冲突的

JDK1.7下的HashMap

1)、HashMap由数组和链表来实现对数据的存储。数组和链表的结合体。HashMap的底层结构是一个数组,数组中的每一项是一条链表。

2)、 HashMap中的key-value都是存储在Entry中的。Entry是实现了Map.Entry接口。

3)、HashMap可以存储null键,只能存储一个null键,放在数组的第一个位置,可以有多个空值。链表存储。

 // 数组变量
 transient HashMap.Entry<K, V>[] table;
//每一项是一个链表
static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        int hash;

        /**
         * Creates new entry.
         */
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }
    }

4)、get(Object key)的过程

// 根据key值获取Entry<K,V>
public V get(Object key) {
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);

        return null == entry ? null : entry.getValue();
}


// 根据key值
final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }

        int hash = (key == null) ? 0 : hash(key);
        // 根据下标确定key在数组的位置,然后取出entry,然后判断key是否相等
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
}
// 下标是通过hash & len - 1 得到的(JDK 1.8中也是一样)
static int indexFor(int h, int length) {
   return h & (length-1);
}

5)、put(K key, V value)的过程。若数组是空的,就需要初始化数组,如果数组不为空,那么就根据h & len-1 来获取到下标的值,若有相同的key则需要进行新值替换旧值,否则就直接创建一个entry即可

public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
}
// 判断容量是否超够了最大的容量限制
private static int roundUpToPowerOf2(int number) {
        // assert number >= 0 : "number must be non-negative";
        return number >= MAXIMUM_CAPACITY
                ? MAXIMUM_CAPACITY
                : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
    }

//初始化数组
private void inflateTable(int toSize) {
        // Find a power of 2 >= toSize
        int capacity = roundUpToPowerOf2(toSize);
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        table = new Entry[capacity];
        initHashSeedAsNeeded(capacity);
}

JDK1.8下的HashMap

1)、底层主要是数组、链表、红黑树来实现的。

当链表的长度大于8的时候,转变为红黑树存储来提高性能。小于6的时候就转换为链表存储

为什么是8的原因:

在理想情况下,链表长度符合泊松分布,在负载因子0.75(HashMap默认)的情况下,单个hash槽内元素个数为8的概率小于千万分之一,通常我们的 Map 里面是不会存储这么多的数据的,所以通常情况下,并不会发生从链表向红黑树的转换。

2)、HashMap中的key-value都是存储在Node中的。Node也是实现了Map.Entry接口

3)、HashMap可以存储null键,只能存储一个null键。在执行put方法的时候,判断hash值时,如果key为null,hash值就是0,所以也是存储在第一个位置的。

// 数组
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
// 链表
static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }

        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
}
// 红黑树节点
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // 父节点
        TreeNode<K,V> left; // 左子节点
        TreeNode<K,V> right; //右子节点
        TreeNode<K,V> prev;    // 前驱节点
        boolean red;
        TreeNode(int hash, K key, V val, Node<K,V> next) {
            super(hash, key, val, next);
		}
}

4)、get(key)方法时获取key的hash值,计算hash&(n-1)得到在链表数组中的位置first=tab[hash&(n-1)],先判断first的key是否与参数key相等,不等就遍历后面的链表找到相同的key值返回对应的Value值即可。


public V get(Object key) {
   Node<K,V> e;
   // 是hash了一次,也是越hashTable获取的时候用的hash值的不同点
   return (e = getNode(hash(key), key)) == null ? null : e.value;
}

static final int hash(Object key) {
   int h;
   return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//获取节点值
final Node<K,V> getNode(int hash, Object key) {
   Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
   // table是存储的节点,也就是Node数组,通过 (n - 1) & hash 来获取下标
   if ((tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & hash]) != null) {
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
       if ((e = first.next) != null) {
       		//如果是红黑树存储节点,那么就在红黑树的存储结构中进行获取
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                if (e.hash == hash &&
                   ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
      }
   }
   return null;
}

5)、put(key,value)的过程:
1,判断键值对数组tab[]是否为空或为null,否则以默认大小resize();

2,根据键值key计算hash值得到插入的数组索引i。tab[hash&(n-1)],如果tab[i]==null,直接新建节点添加,否则转入3

3,判断当前数组中处理hash冲突的方式为链表还是红黑树(check第一个节点类型即可),分别处理

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
        // resize()这个函数进行两个功能,第一个就是初始化数组,第二个就是进行扩容
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
        	//如果是空,那么就进行建立一个新的节点
            tab[i] = newNode(hash, key, value, null);
        else {
        //若已经存在,则就是冲突
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
                // 如果已经是红黑树存储了,那么就以红黑树的结构进行put新值
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
            // 如果不是,那么就是冲突,这里要判断冲突的节点数量是否达到了默认的8,如果是,就需要转换为红黑树
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            // 如果以前的那个key完全一致,就更新那个key的值
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

2. Hashtable和HashMap的区别:

a) 、HashMap线程不安全,HashTable线程安全。

Hashtable的实现方法里面都添加了synchronized关键字来确保线程同步,因此相对而言HashMap性能会高一些,我们平时使用时若无特殊需求建议使用HashMap,在多线程环境下若使用HashMap需要使用Collections.synchronizedMap()方法来获取一个线程安全的集合。

b)、继承不同。

都实现了Map接口,但是HashMap继承了AbstractMap类,HashTable继承了Dictionary类

c) 、Hashtable 中, key 和 value 都不允许出现 null 值。

HashMap 中, null 可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为 null 。

当 get() 方法返回 null 值时,即可以表示 HashMap 中没有该键,也可以表示该键所对应的值为 null 。因此,在 HashMap 中不能由 get() 方法来判断 HashMap 中是否存在某个键, 而应该用 containsKey() 方法来判断。

d) 、 两个遍历方式的内部实现上不同。

Hashtable、HashMap都使用了Iterator。而由于历史原因,Hashtable还使用了Enumeration的方式 。

e) 、哈希值的使用不同。

HashTable直接使用对象的hashCode。而HashMap重新计算hash。

f) 、Hashtable和HashMap它们两个内部实现方式的数组的初始大小和扩容的方式。

HashTable中hash数组默认大小是11,增加的方式是old*2+1
HashMap中hash数组的默认大小是16,以2倍进行扩容,所以一定是2的指数。

注: HashSet子类依靠hashCode()和equal()方法来区分重复元素。 HashSet内部使用Map保存数据,即将HashSet的数据作为Map的key值保存,这也是HashSet中元素不能重复的原因。而Map中保存key值的,会去判断当前Map中是否含有该Key对象,内部是先通过key的hashCode,确定有相同的hashCode之后,再通过equals方法判断是否相同。

HashMap扩容机制

利用resize方法进行扩容,这个方法主要做两件事情:
(1)初始化表。这个不进行介绍了。就是根据默认的容量初始化数据即可。

(2)扩容。(JDK 1.8为例)

a)首先将表的大小扩容至两倍。

b)如果旧表的长度为0。则直接更新一下扩容门限与现在的容量。
如果旧表的长度不为0,则进行新表数据的构造。分为两队,e.hash&oldCap为偶数一队,
e.hash&oldCap为奇数一对

c)返回构造好的新表。

  final Node<K,V>[] resize() {
       Node<K,V>[] oldTab = table;
       int oldCap = (oldTab == null) ? 0 : oldTab.length;
       int oldThr = threshold;
       int newCap, newThr = 0;

/*如果旧表的长度不是空*/
       if (oldCap > 0) {
           if (oldCap >= MAXIMUM_CAPACITY) {
               threshold = Integer.MAX_VALUE;
               return oldTab;
           }
/*把新表的长度设置为旧表长度的两倍,newCap=2*oldCap*/
           else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                    oldCap >= DEFAULT_INITIAL_CAPACITY)
      /*把新表的门限设置为旧表门限的两倍,newThr=oldThr*2*/
               newThr = oldThr << 1; // double threshold
       }
    /*如果旧表的长度的是0,就是说第一次初始化表*/
       else if (oldThr > 0) // initial capacity was placed in threshold
           newCap = oldThr;
       else {               // zero initial threshold signifies using defaults
           newCap = DEFAULT_INITIAL_CAPACITY;
           newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
       }

       if (newThr == 0) {
           float ft = (float)newCap * loadFactor;//新表长度乘以加载因子
           newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                     (int)ft : Integer.MAX_VALUE);
       }
       threshold = newThr;
       @SuppressWarnings({"rawtypes","unchecked"})
/*下面开始构造新表,初始化表中的数据*/
       Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
       table = newTab;//把新表赋值给table
       if (oldTab != null) {//原表不是空要把原表中数据移动到新表中
           /*遍历原来的旧表*/
           for (int j = 0; j < oldCap; ++j) {
               Node<K,V> e;
               if ((e = oldTab[j]) != null) {
                   oldTab[j] = null;
                   if (e.next == null)//说明这个node没有链表直接放在新表的e.hash & (newCap - 1)位置
                       newTab[e.hash & (newCap - 1)] = e;
                   else if (e instanceof TreeNode)
                       ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
/*如果e后边有链表,到这里表示e后面带着个单链表,需要遍历单链表,将每个结点重*/
                   else { // preserve order保证顺序
                新计算在新表的位置,并进行搬运
                       Node<K,V> loHead = null, loTail = null;
                       Node<K,V> hiHead = null, hiTail = null;
                       Node<K,V> next;

                       do {
                           next = e.next;//记录下一个结点
          //新表是旧表的两倍容量,实例上就把单链表拆分为两队,
             //e.hash&oldCap为偶数一队,e.hash&oldCap为奇数一对
                           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) {//lo队不为null,放在新表原位置
                           loTail.next = null;
                           newTab[j] = loHead;
                       }
                       if (hiTail != null) {//hi队不为null,放在新表j+oldCap位置
                           hiTail.next = null;
                           newTab[j + oldCap] = hiHead;
                       }
                   }
               }
           }
       }
       return newTab;
   }

线程不安全的体现

1、HashMap是非线程安全(PUT、GET)

1)首先是PUT的不安全性:

源码:处理逻辑:

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab;
    Node<K,V> p;
    int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
    /*如果table的在(n-1)&hash的值是空,就新建一个节点插入在该位置*/
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
    /*表示有冲突,开始处理冲突*/
        else {
            Node<K,V> e;
        K k;
    /*检查第一个Node,p是不是要找的值*/
            if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
        /*指针为空就挂在后面*/
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
               //如果冲突的节点数已经达到8个,看是否需要改变冲突节点的存储结构,
            //treeifyBin首先判断当前hashMap的长度,如果不足64,只进行
                        //resize,扩容table,如果达到64,那么将冲突的存储结构为红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
        /*如果有相同的key值就结束遍历*/
                    if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
    /*就是链表上有相同的key值*/
            if (e != null) { // existing mapping for key,就是key的Value存在
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;//返回存在的Value值
            }
        }
        ++modCount;
     /*如果当前大小大于门限,门限原本是初始容量*0.75*/
        if (++size > threshold)
            resize();//扩容两倍
        afterNodeInsertion(evict);
        return null;
    }

下面将举两个可能出现线程不安全的地方。

PUT的时候导致的多线程数据不一致。

这个问题比较好想象,比如有两个线程A和B,首先A希望插入一个key-value对到HashMap中,首先计算记录所要落到的桶的索引坐标,然后获取到该桶里面的链表头结点,此时线程A的时间片用完了,而此时线程B被调度得以执行,和线程A一样执行,只不过线程B成功将记录插到了桶里面,假设线程A插入的记录计算出来的桶索引和线程B要插入的记录计算出来的桶索引是一样的,那么当线程B成功插入之后,线程A再次被调度运行时,它依然持有过期的链表头但是它对此一无所知,以至于它认为它应该这样做,如此一来就覆盖了线程B插入的记录,这样线程B插入的记录就凭空消失了,造成了数据不一致的行为。

2)GET引起的线程不安全

public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
    
final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab;//Entry对象数组
    Node<K,V> first,e; //在tab数组中经过散列的第一个位置
    int n;
    K k;
    /*找到插入的第一个Node,方法是hash值和n-1相与,tab[(n - 1) & hash]*/
    //也就是说在一条链上的hash值相同的
        if ((tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & hash]) != null) {
    /*检查第一个Node是不是要找的Node*/
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))//判断条件是hash值要相同,key值要相同
                return first;
      /*检查first后面的node*/
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                /*遍历后面的链表,找到key值和hash值都相同的Node*/
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

线程不安全的问题是HashMap的get操作可能因为resize而引起死循环(cpu100%)

2、Hashtable线程安全

采用synchronized关键字解决了并发访问的安全性问题但是效率较低

public synchronized V get(Object key) {
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
        if ((e.hash == hash) && e.key.equals(key)) {
            return (V)e.value;
        }
    }
    return null;
}

3、ConcurrentHashMap线程安全

ConcurrentHashMap底层采用的锁机制,执行put方法的线程会获得锁,只有当此线程的put方法执行结束后才会释放锁,根据多线程的知识,获得锁的线程会通知其他试图操作put方法的线程,并通知其他线程出于等待状态,直到释放锁后,其他线程才会去重新竞争锁。这一点保证了ConcurrentHashMap的线程安全。

底层采用分段的数组+链表实现,线程安全

通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。(读操作不加锁,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。)

Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术

有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁

扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容),插入前检测需不需要扩容,有效避免无效扩容

1)、使用了volatile

transient volatile Node<K,V>[] table;

线程对变量的所有操作都必须在线程自己的工作内存中完成,而不能直接读取主存中的变量,这是JVM的规定。所以每个线程都会有自己的工作内存,工作内存中存放了共享变量的副本。而正是因为这样,才造成了可见性的问题。

ABCD四个线程同时在操作一个共享变量X,此时如果A从主存中读取了X,改变了值,并且写回了内存。那么BCD线程所得到的X副本就已经失效了。此时如果没有被volatile修饰,那么BCD线程是不知道自己的变量副本已经失效了。继续使用这个变量就会造成数据不一致的问题。

科普:内存可见性
如果加上了volatile关键字,BCD线程就会立马看到最新的值,这就是内存可见性。你可能想问,凭什么加了volatile的关键字就可以保证共享变量的内存可见性?
那是因为如果变量被volatile修饰,在线程进行写操作时,会直接将新的值写入到主存中,而不是线程的工作内存中;而在读操作时,会直接从主存中读取,而不是线程的工作内存。

2)、CAS操作:CAS操作保证了设置sizeCtl标记位的原子性,保证了只有一个线程能设置成功
关于PUT,初始未空就initTable

final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            //判断Node数组为空
            if (tab == null || (n = tab.length) == 0)
                //初始化Node数组
                tab = initTable();
          ...
}

initTable

private transient volatile int sizeCtl;

private final Node<K,V>[] initTable() {
  Node<K,V>[] tab; int sc;
  //每次循环都获取最新的Node数组引用
  while ((tab = table) == null || tab.length == 0) {
    //sizeCtl是一个标记位,若为-1也就是小于0,代表有线程在进行初始化工作了
    if ((sc = sizeCtl) < 0)
      //让出CPU时间片
      Thread.yield(); // lost initialization race; just spin
    //CAS操作,将本实例的sizeCtl变量设置为-1
    else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
      //如果CAS操作成功了,代表本线程将负责初始化工作
      try {
        //再检查一遍数组是否为空
        if ((tab = table) == null || tab.length == 0) {
          //在初始化Map时,sizeCtl代表数组大小,默认16
          //所以此时n默认为16
          int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
          @SuppressWarnings("unchecked")
          //Node数组
          Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
          //将其赋值给table变量
          table = tab = nt;
          //通过位运算,n减去n二进制右移2位,相当于乘以0.75
          //例如16经过运算为12,与乘0.75一样,只不过位运算更快
          sc = n - (n >>> 2);
        }
      } finally {
        //将计算后的sc(12)直接赋值给sizeCtl,表示达到12长度就扩容
        //由于这里只会有一个线程在执行,直接赋值即可,没有线程安全问题
        //只需要保证可见性
        sizeCtl = sc;
      }
      break;
    }
  }
  return tab;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值