最新要闻

广告

手机

iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?

iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?

警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案

警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案

家电

【打怪升级】【容器】关于Map

来源:博客园
关于Map接口,具体的实现有HashMap、HashTable、TreeMap等
  • HashMap

老规矩,如果我们要看源码,我们要从这么几点去看:它的继承结构、它的核心实现能力。我们知道hashMap是一个kv容器,那么它的实现其实主要取决于这几点:

1.存放 如何处理hash冲突 怎么存?

2.获取 怎么通过key获取?


(资料图片)

3.扩容 什么时候 什么条件会扩容?

4.删除 怎么样删除一个元素

非常简洁的继承结构,除了序列化和拷贝支持还继承了AbstractMap复用key,value结构的部分通用功能。

我们先来看一下hashmap的结构

首先说明一下,(n - 1) % hash 和 (n - 1) & hash,(2^h - 1) & hash 是等价的,其中n = 2^h,后者运算效率更高。

通过hashMap的继承结构,我们可以发现首先它实现了上层Map,并且实现了AbstractMap,HashMap内部采用的是数组+链表的结构,作为元素的存放,在存入的时候会先进行(n - 1) & hash。

HashMap采用了数组加链表的结构,在存入元素的时候,会先通过(n - 1) & hash获取hash存储的位置,这个位置就是key存放的位置,其实就是(n - 1) % hash,n相当于数组的长度,因为要&运算,所以长度必须是2的n次幂。

只有当hash发生了冲突,元素的内容不一样,但是hash值一样,这时候两个元素通过(n - 1) & hash均放在了同一个位置,这时候才会在链表中放入元素。

那么。在hashMap中,Node又是用来干什么的呢?

数组中每个元素都被包装成了Node,其内部有四个属性,详情看源码:

/* ---------------- Fields -------------- */    /**     * The table, initialized on first use, and resized as     * necessary. When allocated, length is always a power of two.     * (We also tolerate length zero in some operations to allow     * bootstrapping mechanics that are currently not needed.)     */
//这个就是存储元素的数组
transient Node[] table;
static class Node implements Map.Entry {      //key hash        final int hash;     // 元素的key         final K key;     //元素的value        V value;     //下一个node指针        Node next;        Node(int hash, K key, V value, Node 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;        }    }

当发生了hash冲突、元素内容不一样但是hash一样,才会在链表中放入元素,数组中的元素被Node进行包装,内部分别是key、value、hash、和next指针。

那么,hashmap是如何进行hash计算的呢?

static final int hash(Object key) {        int h;        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}

key的hash计算并没有我们想想的那么复杂,首先null的hash永远为0,其次是将元素本身的hashCode取反,再与高16位异或计算。我们知道hashCode返回的是int值,所以最大就是32个字节。由于计算下标的时候是,(n-1) & hash。所以当n - 1,比较小的时候,只能与hash 的低位计算。比如数字 786431 和 1835007 的低位全都是1,但是他们的高位却不相同。这个时候让高位参与计算,会进一步减少碰撞的概率。

那么了解到hashmap的基本结构后,我们要从源码的结构和方法进一步了解它

首先看看HashMap的成员变量

//默认的数组长度 16static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //数组最大长度 1073741824static final int MAXIMUM_CAPACITY = 1 << 30;//默认的扩容因子static final float DEFAULT_LOAD_FACTOR = 0.75f;//当链表的长度大于这个数字的时候,会转换成数结构static final int TREEIFY_THRESHOLD = 8;//存放数据的数组,内部都是一个个链表transient Node[] table;//entrySet的引用,会在调用entrySet()时候赋值transient Set> entrySet;//这个是Map内的元素个数,每put一个元素进来就会+1。当size大于threshold的时候就会扩容transient int size;//防止并发修改的计数器transient int modCount;//扩容的阈值//当数组初始化的时候会被用来记录初始化数组长度,这个时候他的长度就是数组的长度。其他的时候就和数组长度没哈关系了int threshold;//扩容因子,默认值是 DEFAULT_LOAD_FACTOR = 0.75。用来计算扩容的阈值final float loadFactor;

HashMap的构造方法

public HashMap(int initialCapacity, float loadFactor) {        if (initialCapacity < 0)            throw new IllegalArgumentException("Illegal initial capacity: " +                                               initialCapacity);        if (initialCapacity > MAXIMUM_CAPACITY)            initialCapacity = MAXIMUM_CAPACITY;        if (loadFactor <= 0 || Float.isNaN(loadFactor))            throw new IllegalArgumentException("Illegal load factor: " +                                               loadFactor);        this.loadFactor = loadFactor;    //这里tablesizefor 很重要!!! 它通过获取最近的2^n,指定初始化长度        this.threshold = tableSizeFor(initialCapacity);    }    /**     * Constructs an empty HashMap with the specified initial     * capacity and the default load factor (0.75).     *     * @param  initialCapacity the initial capacity.     * @throws IllegalArgumentException if the initial capacity is negative.     */    public HashMap(int initialCapacity) {     //指定长度,扩容因子0.75        this(initialCapacity, DEFAULT_LOAD_FACTOR);    }    /**     * Constructs an empty HashMap with the default initial capacity     * (16) and the default load factor (0.75).     */    public HashMap() {     //默认长度16 扩容因子0.75        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted    }    /**     * Constructs a new HashMap with the same mappings as the     * specified Map.  The HashMap is created with     * default load factor (0.75) and an initial capacity sufficient to     * hold the mappings in the specified Map.     *     * @param   m the map whose mappings are to be placed in this map     * @throws  NullPointerException if the specified map is null     */    public HashMap(Map m) {        this.loadFactor = DEFAULT_LOAD_FACTOR;        putMapEntries(m, false);    }
static final int tableSizeFor(int cap) {        int n = cap - 1;        n |= n >>> 1;        n |= n >>> 2;        n |= n >>> 4;        n |= n >>> 8;        n |= n >>> 16;        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;    }
当cap正好就是2^n时 不减1会直接扩大一倍    之前说了cap最近的2^n数字,比如 3 离它最近的是4,那么对于4呢?其实离他最近的是8。    如果直接运算,会扩大一倍,所以将4减1,变成3,也就是离3最近的就是4。
n溢出,或者达到最大容量。正常情况下将n+1还原

扩容

扩容,是hashmap的一个核心方法,这个跟其他容器一样,为了节约空间。所以我们在业务开发中,往往在特定长度需要指定它的长度避免它频繁扩容!!!

final Node[] resize() {    //记住当前的元素数组    Node[] oldTab = table;    //当前的数组容量,没有初始化就是0    int oldCap = (oldTab == null) ? 0 : oldTab.length;    //当前的数组的分配长度    int oldThr = threshold;    //newCap是新的数组长度和newThr 是新的数组容量    int newCap, newThr = 0;    //原来的数组不是空的    if (oldCap > 0) {        //原来的数组已经达到最大长度限制        if (oldCap >= MAXIMUM_CAPACITY) {            //直接将分配长度扩大成最大值,其实就是不能扩容了。            threshold = Integer.MAX_VALUE;            return oldTab;        }        //如果不是初始容量,老的数组长度扩大2倍 并且还在最大容量内        //并且老的容量已经超过了默认的初始值16        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&                 oldCap >= DEFAULT_INITIAL_CAPACITY)            //新的阈值是原来的2倍            newThr = oldThr << 1;     }    //初始化的时候指定了容量,直接使用指定的容量初始化    //这里比较绕,因为它构造里边用的是threshold来记录的初始化长度    //所以后边还会有 newThr == 0 的情况    else if (oldThr > 0)         newCap = oldThr;    else {       //采用默认的数据初始化,数组长度是 16,扩容因子是0.75。阈值是12        newCap = DEFAULT_INITIAL_CAPACITY;        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);    }    //这里是,初始化的时候指定了容量    //或者现有的容量小于 DEFAULT_INITIAL_CAPACITY 或者 扩容后超出MAXIMUM_CAPACITY 是触发    if (newThr == 0) {        //新的数组扩容阈值        float ft = (float)newCap * loadFactor;        //确定新的阈值,新的容量小于MAXIMUM_CAPACITY 并且临界值 也小于MAXIMUM_CAPACITY 才会用ft        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?                  (int)ft : Integer.MAX_VALUE);    }    //新的阈值    threshold = newThr;    @SuppressWarnings({"rawtypes","unchecked"})        //使用新的容量去初始化一个新的Node数组,newCap必须是2^n        Node[] newTab = (Node[])new Node[newCap];    table = newTab;    //处理旧数组里的数组    if (oldTab != null) {        //遍历原来数组的每个元素        for (int j = 0; j < oldCap; ++j) {            Node e;            if ((e = oldTab[j]) != null) {                oldTab[j] = null;                if (e.next == null)                    //如果只有一个元素,将元素的hash与现在的位置取余确定新的数组下标                    newTab[e.hash & (newCap - 1)] = e;                else if (e instanceof TreeNode)                    //对于数结构的处理                    ((TreeNode)e).split(this, newTab, j, oldCap);                else {                     //这个是将原始的链表拆分开,分成高位和低位。                    //低位不用移动,高位放到现有的位置 + 数组的原始长度处                    Node loHead = null, loTail = null;                    Node hiHead = null, hiTail = null;                    Node next;                    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;                    }                    //将高位元素放在原始的索引 + 原始的数组长度处                    if (hiTail != null) {                        hiTail.next = null;                        newTab[j + oldCap] = hiHead;                    }                }            }        }    }    //返回新的元素    return newTab;}

对于新数组容量和扩容阈值;

数组中链表只有一个元素,那么直接存储;

数组中是一个红黑树结构;

数组中还没有到达红黑树结构、但是有多个、拆分链表;

其中,HashMap提供了TreeNode,当数组内链表长度大于8后转换为TreeNode。

其中,默认的扩容因子是0.75,意味着:当添加某个元素后,数组的总的添加元素数大于了 数组长度 * 0.75(默认,也可自己设定),数组长度扩容为两倍。(如开始创建HashMap集合后,数组长度为16,临界值为16 * 0.75 = 12,当加入元素后元素个数超过12,数组长度扩容为32,临界值变为24)

Map只有在添加的时候才会扩容,删除的时候是不会缩容的

接下来,我们看看针对容器的操作方法:

添加元素:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,               boolean evict) {    Node[] tab; Node p; int n, i;    //如果table 还没初始化,调用resize()初始化一个数组    if ((tab = table) == null || (n = tab.length) == 0)        n = (tab = resize()).length;    //通过(n - 1) & hash 计算元素的下标,如果下标出没有元素,直接初始化一个    if ((p = tab[i = (n - 1) & hash]) == null)        tab[i] = newNode(hash, key, value, null);    else {        //下标出已经有元素了,p就是下标处链表的头        Node e; K k;        //当前元素的hash和下标处表头的hash相等,并且他们的key相等,直接辅助e        //这其实是个优化措施,比如你put一个key已经存在的元素        if (p.hash == hash &&            ((k = p.key) == key || (key != null && key.equals(k))))            e = p;        //如果p已经转化成树结构,树结构处理        else if (p instanceof TreeNode)            e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);        else {            //以p为起点,循环链表            for (int binCount = 0; ; ++binCount) {                if ((e = p.next) == null) {                    //如果链表的尾部还没找到,就添加一个元素进去                    p.next = newNode(hash, key, value, null);                    //是否有必要转换成树结构 增强查询效率                    if (binCount >= TREEIFY_THRESHOLD - 1)                         treeifyBin(tab, hash);                    break;                }                //如果在中间找到了,记下这个节点                if (e.hash == hash &&                    ((k = e.key) == key || (key != null && key.equals(k))))                    break;                p = e;            }        }        //当前key对应的节点已经存在        if (e != null) {             //将原来节点里的值替换掉            V oldValue = e.value;            //原来的值是null或者明确指定了onlyIfAbsent 的才更新            if (!onlyIfAbsent || oldValue == null)                e.value = value;            //模板方法,里边是个空实现            afterNodeAccess(e);            return oldValue;        }    }    //操作次数加1    ++modCount;    //元素数量+1,并且与扩容阈值比较    if (++size > threshold)        resize();    //模板方法,里边是个空实现    afterNodeInsertion(evict);    return null;}

总体来说,HashMap添加元素可以分为这几步:

1.初始化

2.查找元素位置,如果当前位置是空,那么直接放,如果不是空就遍历链表看看元素是否存在?存在就更新,不存在则添加

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

这些都是基于putVal去实现的。

如果通过别的方式添加元素,例如putMapEntries:

final void putMapEntries(Map m, boolean evict) {    //获取待添加的map元素个数    int s = m.size();    if (s > 0) {        if (table == null) {             //反算一下容量的大小            float ft = ((float)s / loadFactor) + 1.0F;            int t = ((ft < (float)MAXIMUM_CAPACITY) ?                     (int)ft : MAXIMUM_CAPACITY);            //已经超过了容量的阈值            if (t > threshold)                //将容量阈值规整为2^n形式                threshold = tableSizeFor(t);        }        //如果超限了 直接去扩容,确定一个合适的容量        else if (s > threshold)            resize();        for (Map.Entry e : m.entrySet()) {            K key = e.getKey();            V value = e.getValue();            //使用putVal一个一个添加            putVal(hash(key), key, value, false, evict);        }    }}//快捷方法public void putAll(Map m) {    putMapEntries(m, true);}

删除元素:

final Node removeNode(int hash, Object key, Object value,                           boolean matchValue, boolean movable) {    Node[] tab; Node p; int n, index;    //只有table不为空,并且存在数据的时候才处理    if ((tab = table) != null && (n = tab.length) > 0 &&        (p = tab[index = (n - 1) & hash]) != null) {        Node node = null, e; K k; V v;        //这里是为了key对应的元素位置,一样是个优化策略。直接看第一个位置是不是要找的元素        if (p.hash == hash &&            ((k = p.key) == key || (key != null && key.equals(k))))            node = p;        else if ((e = p.next) != null) {            //数结构单独处理            if (p instanceof TreeNode)                node = ((TreeNode)p).getTreeNode(hash, key);            else {                //循环列表,看能不能找到key对应的位置                do {                    if (e.hash == hash &&                        ((k = e.key) == key ||                         (key != null && key.equals(k)))) {                        node = e;                        break;                    }                    p = e;                } while ((e = e.next) != null);            }        }        //通过上边的查找,如果找到了key对应的node节点        //如果启用了value匹配,并且value是匹配的。就准备删除节点         if (node != null && (!matchValue || (v = node.value) == value ||                             (value != null && value.equals(v)))) {            //树节点单独处理            if (node instanceof TreeNode)                ((TreeNode)node).removeTreeNode(this, tab, movable);            else if (node == p)                //如果第一个节点就是node,直接替换掉第一个元素                tab[index] = node.next;            else                //直接替换next指针到node的下一个节点                p.next = node.next;            //操作次数增加            ++modCount;            //元素数量减少            --size;            //模板方法,其实是个空实现            afterNodeRemoval(node);            return node;        }    }    return null;}

删除元素的动作也可以分为两步:

1.找到key对应的位置,且hash和key都相等

2.处理结构,数组还是链表?

public V remove(Object key) {    Node e;    return (e = removeNode(hash(key), key, null, false, true)) == null ?        null : e.value;}public boolean remove(Object key, Object value) {    return removeNode(hash(key), key, value, true, true) != null;}

以上都是通过removeNode实现。

清空元素:

public void clear() {    Node[] tab;    modCount++;    if ((tab = table) != null && size > 0) {        size = 0;        //直接把数组的元素置空,数组的大小不变        for (int i = 0; i < tab.length; ++i)            tab[i] = null;    }}

查找元素:

这是比较核心,也比较粗暴的方式

final Node getNode(int hash, Object key) {    Node[] tab; Node first, e; int n; K k;    //table不是空的,并且key对应的下标是有数据存在的    if ((tab = table) != null && (n = tab.length) > 0 &&        (first = tab[(n - 1) & hash]) != null) {        //优化策略,首先检查第一个节点不是要找e 的key,如果是直接返回。        if (first.hash == hash &&             ((k = first.key) == key || (key != null && key.equals(k))))            return first;        //链表不止一个元素        if ((e = first.next) != null) {            //树结构单独处理            if (first instanceof TreeNode)                return ((TreeNode)first).getTreeNode(hash, key);            do {                //循环链表一直找到key                if (e.hash == hash &&                    ((k = e.key) == key || (key != null && key.equals(k))))                    return e;            } while ((e = e.next) != null);        }    }    return null;}

查找元素,就过于粗暴了。当然为了效率,会在链表长度大于8时自动转化为红黑树结构。

public V get(Object key) {    Node e;    return (e = getNode(hash(key), key)) == null ? null : e.value;}public boolean containsKey(Object key) {    return getNode(hash(key), key) != null;}//不存在元素的时候,使用一个默认值替换public V getOrDefault(Object key, V defaultValue) {    Node e;    return (e = getNode(hash(key), key)) == null ? defaultValue : e.value;}

这些都是通过getNode实现。

修改元素:

public boolean replace(K key, V oldValue, V newValue) {    Node e; V v;    //直接获取元素,如果是空的或者和oldValue不想等,就不处理    if ((e = getNode(hash(key), key)) != null &&        ((v = e.value) == oldValue || (v != null && v.equals(oldValue)))) {        e.value = newValue;        afterNodeAccess(e);        return true;    }    return false;}//和上边一样,只是少了旧值的比较操作public V replace(K key, V value) {    Node e;    if ((e = getNode(hash(key), key)) != null) {        V oldValue = e.value;        e.value = value;        afterNodeAccess(e);        return oldValue;    }    return null;}public void replaceAll(BiFunction function) {    Node[] tab;    if (function == null)        throw new NullPointerException();    if (size > 0 && (tab = table) != null) {        int mc = modCount;        //先循环数组        for (int i = 0; i < tab.length; ++i) {            //在循环数组内的链表            for (Node e = tab[i]; e != null; e = e.next) {                //使用function替换每个元素                e.value = function.apply(e.key, e.value);            }        }        //如果发生了并发修改,就报错        if (modCount != mc)            throw new ConcurrentModificationException();    }}

遍历容器:

HashIterator 是针对内部key和value的迭代器实现

abstract class HashIterator {    Node next;        // next entry to return    Node current;     // current entry    int expectedModCount;  // for fast-fail    int index;             // current slot    HashIterator() {        //记录当前被操作数量        expectedModCount = modCount;        //table引用        Node[] t = table;        current = next = null;        index = 0;        //这个是为找到数组中第一个不是空的链表        if (t != null && size > 0) { // advance to first entry            do {} while (index < t.length && (next = t[index++]) == null);        }    }    public final boolean hasNext() {        return next != null;    }    final Node nextNode() {        Node[] t;        Node e = next;        //并发操作检测        if (modCount != expectedModCount)            throw new ConcurrentModificationException();        //如果数组中存在空的就报错        if (e == null)            throw new NoSuchElementException();        //获取到下一个不是空的元素,并赋值给current 和 next        if ((next = (current = e).next) == null && (t = table) != null) {            do {} while (index < t.length && (next = t[index++]) == null);        }        return e;    }        //就是用的removeNode 其实只是加了并发检测    public final void remove() {        Node p = current;        if (p == null)            throw new IllegalStateException();        if (modCount != expectedModCount)            throw new ConcurrentModificationException();        current = null;        K key = p.key;        removeNode(hash(key), key, null, false, false);        expectedModCount = modCount;    }}

KeySet

public Set keySet() {    Set ks = keySet;    if (ks == null) {        //创建一个KeySet引用        ks = new KeySet();        keySet = ks;    }    return ks;}final class KeySet extends AbstractSet {    public final int size()                 { return size; }    public final void clear()               { HashMap.this.clear(); }    //这个就是for循环语法糖会用的    public final Iterator iterator()     { return new KeyIterator(); }    public final boolean contains(Object o) { return containsKey(o); }    public final boolean remove(Object key) {        return removeNode(hash(key), key, null, false, true) != null;    }    public final Spliterator spliterator() {        return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0);    }    public final void forEach(Consumer action) {        Node[] tab;        if (action == null)            throw new NullPointerException();        if (size > 0 && (tab = table) != null) {            int mc = modCount;            //先循环数组            for (int i = 0; i < tab.length; ++i) {                //再循环链表,但是只寻找元素的key                for (Node e = tab[i]; e != null; e = e.next)                    action.accept(e.key);            }            if (modCount != mc)                throw new ConcurrentModificationException();        }    }}//继承了上边的HashIterator,next里只keyfinal class KeyIterator extends HashIterator    implements Iterator {    public final K next() { return nextNode().key; }}

Values

public Collection values() {    Collection vs = values;    if (vs == null) {        //使用Values,和keySet机会        vs = new Values();        values = vs;    }    return vs;}final class Values extends AbstractCollection {    public final int size()                 { return size; }    public final void clear()               { HashMap.this.clear(); }    //这个就是for循环语法糖会用的    public final Iterator iterator()     { return new ValueIterator(); }    public final boolean contains(Object o) { return containsValue(o); }    public final Spliterator spliterator() {        return new ValueSpliterator<>(HashMap.this, 0, -1, 0, 0);    }    public final void forEach(Consumer action) {        Node[] tab;        if (action == null)            throw new NullPointerException();        if (size > 0 && (tab = table) != null) {            int mc = modCount;            for (int i = 0; i < tab.length; ++i) {                for (Node e = tab[i]; e != null; e = e.next)                    action.accept(e.value);            }            if (modCount != mc)                throw new ConcurrentModificationException();        }    }}//继承了上边的HashIterator,next里只valuefinal class ValueIterator extends HashIterator    implements Iterator {    public final V next() { return nextNode().value; }}

快速失败

8版本之后的快速失败:HashMap提供了一个modCount来定义修改的次数,如果常规遍历的情况下,再进行操作时会修改modCount的值,但是不会修改expectedModCount,每次操作时只要它两不相等就会出现ConcurrentModificationException异常。如果使用迭代器Iterator本身的操作例如remove方法操作时,会同时更新expectedModCount,那么就不会抛出ConcurrentModificationException的异常了。

树化

TreeNode是HashMap的内部类,当数组内链表的长度大于8的时候会转换为TreeNode,进一步加强查找效率

首先,为什么HashMap要采用红黑树呢?具体我们可以参考红黑树的数据结构模型。这里不做过多解释,但是要明确一点,红黑树的特性就是从根到叶子节点的长度不会过长,它们是一个相对平衡的状态,哪怕有大量的数据,也能避免在一个根节点下大量遍历查找。从而提高查找效率。

但是通过添加到指定长度以上,会进入treeifyBin方法,这个方法会将Node节点转为TreeNode节点。注意!!!!因为Node有指针的,这里还是上下节点,所以它还没有达到一个红黑树的标准。

我们都知道看一个树形结构,我们要通过它的继承看到它的特性。我们来看看Node和TreeNode的区别:

static final class TreeNode extends LinkedHashMap.Entry static class Node implements Map.Entry

它们都基于KV存储实现,但是不一样的是TreeNode又继承了linkedHashMap。

final void treeifyBin(Node[] tab, int hash) {        int n, index; Node e;        //如果当前列表小于64 就进行扩容就够了        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)            resize();        else if ((e = tab[index = (n - 1) & hash]) != null) {            //如果已经大于64了,先将链表转化为树结构,注意此时还不是红黑树 这些树的元素通过next指针相连            TreeNode hd = null, tl = null;            do {                //先生成TreeNode节点                TreeNode p = replacementTreeNode(e, null);                if (tl == null)                    //先将head 设置为第一个TreeNode                    hd = p;                else {                    //新插入的TreeNode 的parent 就是上一个Node 如果前面没有那就是header                    p.prev = tl;                    tl.next = p;                }                //尾设置为最新插入的节点                tl = p;            } while ((e = e.next) != null);            if ((tab[index] = hd) != null)                //树化                hd.treeify(tab);        }    }

通过treeifyBin我们可以发现,这里开始处理TreeNode了,但此时它应该称为树,而不是红黑树,因为它们的节点是相连的

final void treeify(Node[] tab) {            TreeNode root = null;            //遍历树节点            for (TreeNode x = this, next; x != null; x = next) {                //处理第一个树节点                next = (TreeNode)x.next;                x.left = x.right = null;                if (root == null) {                    //如果当前没有根节点,那么它就当根节点 它是一个black节点                    x.parent = null;                    x.red = false;                    root = x;                }                else {                    K k = x.key;                    int h = x.hash;                    Class kc = null;                    for (TreeNode p = root;;) {                        //如果已经有根节点,那么就要遍历处理向左或者向右偏移                        int dir, ph;                        K pk = p.key;                        //如果hash大于插入节点的hash 那么就要去左树继续查找                        if ((ph = p.hash) > h)                            dir = -1;                        //右边查找 同理                        else if (ph < h)                            dir = 1;                        //如果相同,就单独处理                        else if ((kc == null &&                                  (kc = comparableClassFor(k)) == null) ||                                 (dir = compareComparables(kc, k, pk)) == 0)                            dir = tieBreakOrder(k, pk);                        TreeNode xp = p;                        //根据dir 就知道到底在左还是在右                        //如果当前为空了,那么就应该将它插入了                        if ((p = (dir <= 0) ? p.left : p.right) == null) {                            //修改指针                            x.parent = xp;                            if (dir <= 0)                                xp.left = x;                            else                                xp.right = x;                            //旋转处理                            root = balanceInsertion(root, x);                            break;                        }                    }                }            }            moveRootToFront(tab, root);        }

那么在数据找到位置并插入后,就要做红黑树的处理balanceInsertion

static  TreeNode balanceInsertion(TreeNode root,                                                    TreeNode x) {            x.red = true;            for (TreeNode xp, xpp, xppl, xppr;;) {                if ((xp = x.parent) == null) {                    x.red = false;                    return x;                }                else if (!xp.red || (xpp = xp.parent) == null)                    return root;                if (xp == (xppl = xpp.left)) {                    if ((xppr = xpp.right) != null && xppr.red) {                        xppr.red = false;                        xp.red = false;                        xpp.red = true;                        x = xpp;                    }                    else {                        if (x == xp.right) {                            root = rotateLeft(root, x = xp);                            xpp = (xp = x.parent) == null ? null : xp.parent;                        }                        if (xp != null) {                            xp.red = false;                            if (xpp != null) {                                xpp.red = true;                                root = rotateRight(root, xpp);                            }                        }                    }                }                else {                    if (xppl != null && xppl.red) {                        xppl.red = false;                        xp.red = false;                        xpp.red = true;                        x = xpp;                    }                    else {                        if (x == xp.left) {                            root = rotateRight(root, x = xp);                            xpp = (xp = x.parent) == null ? null : xp.parent;                        }                        if (xp != null) {                            xp.red = false;                            if (xpp != null) {                                xpp.red = true;                                root = rotateLeft(root, xpp);                            }                        }                    }                }            }        }

这里是将节点旋转处理,做左旋转或右旋转,这里对这个不做过多解释,有兴趣可以根据源码看看如何转换的,其中在插入数据时如果检查到是TreeNode,就根据putTreeVal处理,这里的方法类似。

其中如果是treeNode进行查找 也是根据树形查找方式,这里如果对数据结构比较熟悉的朋友应该已经知道了

final TreeNode getTreeNode(int h, Object k) {            return ((parent != null) ? root() : this).find(h, k, null);        }
  • ConcurrentHashMap

ConcurrentHashMap是juc下的一个线程安全容器,主要是为了解决HashMap线程不安全的场景。

在jdk7中,ConcurrentHashMap保证线程安全的方式就是Segment,分段锁,默认Segment是16个,分段加锁保证读写效率。可以在初始化时修改

每个Segment中,我们可以看作每个Segment里面就是一个HashMap的结构,每个Segment都持有了独立的ReentrantLock,关于ReentrantLock可以参考aqs的原理。

所以在操作每个Segment时,互相是不干扰的。

但是在jdk8里,将Segment换成了Node,每个Node有自己的锁,通过CAS原理。

简单了解一下7中的原理

static final class Segment extends ReentrantLock implements Serializable {    private static final long serialVersionUID = 2249069246763182397L;    static final int MAX_SCAN_RETRIES =        Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;    transient volatile HashEntry[] table;    transient int count;    transient int modCount;    transient int threshold;    final float loadFactor;}final Segment[] segments;

HashEntry(哈希数组),threshold(扩容阈值),loadFactor(负载因子)表示segment是一个完整的HashMap。

public ConcurrentHashMap(int initialCapacity,float loadFactor, int concurrencyLevel)

初始容量:初始容量表示所有的segment数组中,一共含有多少个hashentry。若initialCapacity不为2的幂,会取一个大于initialCapacity的2的幂。  负载因子:默认0.75  并发级别:可以同时允许多少个线程并发。concurrencyLevel为多少,就有多少个segment,当然也会取一个大于等于这个值的2的幂。

而在8中,其实采用的是Node数组+链表+红黑树,例如put方法就是通过CAS原理 比较替换的方式添加。

那8下的ConcurrentHashMap如何保证线程安全的呢?其实是使用Unsafe操作+synchronized关键字;

synchronized在操作的位置进行加锁、比如我们向某个链表插入数据,那就会在Node上先同步、然后通过CAS插入。在8版本下其实也有分段的思想、但是在7中其实我们指定了桶的数量、而在8中认为是每个Node的每个位置都有一把锁

如果我们这时去put 一个key-value

根据key计算数组下标、如果没有元素,则CAS添加;

如果有元素 那么先synchronized锁定;

加锁成功,判断元素类型,如果是链表那么就添加到链表、如果是红黑树节点那么添加到TreeNode;

添加完成,需要看是否需要转化成树结构;

  • HashTable

hashTable跟HashMap最大的区别,就是HashTable是保证线程安全的,它的所有操作都进行了加锁。

HashMap底层是一个Entry数组,当发生hash冲突的时候,hashmap是采用链表的方式来解决的,在对应的数组位置存放链表的头结点。对链表而言,新加入的节点会从头结点加入。在hashmap做put操作的时候可能会造成数据丢失。现在假如A线程和B线程同时对同一个数组位置调用addEntry,两个线程会同时得到现在的头结点,然后A写入新的头结点之后,B也写入新的头结点,那B的写入操作就会覆盖A的写入操作造成A的写入操作丢失。

  • TreeMap

对于treeMap,很多人很陌生、它也是基于Map接口实现的。

TreeMap是一个基于key有序的key value的散列。

map根据创建的key的排序方式、或者重写Comparator的排序方式进行排序;底层还是基于红黑树的结构实现的。

从它的继承结构可以清晰的看出来,它又实现了sorted排序方式,所以它是一个有序的。

关键词: