2-1 HashMap-ConcurrentHashMap

java基础
java集合
JVM
多线程
mysql_数据库
计算机网络
nosql_redis
设计模式
操作系统
消息中间件activeMq
SSM框架面试题
服务中间件Dubbo

注意: 本文设计的原理和源码大部分都是jdk1.8的,不过我会给出1.7的源码。注意查看 我会标记清楚的。

1-HashMap

1-1原理

hashmap是数组+链表或红黑树(1.8新增)结合体,数组的每个元素存储的是链表的头结点。(1.8)向hashmap put值的时候,先取key.hashcode 在hash & length -1计算出数组下标。

if 该下标对应的链表为空,直接把键与值作为链表头节点。不为空,则遍历链表是否存在于key相同的节点,{ 相同,value值替换。 不相同,则放在尾部。1.8
}

1.7是头插法,而1.8是尾插法,待会我给你们解释为什么要这样做。

1-2hash函数的计算

1.8 直接上源码

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

下图为计算过程 ,对key。
在这里插入图片描述

1.7可不是这样的哦 没有 1.8简单,而且1.8更方便易懂。

1.7
final int hash(Object k) {
        int h = hashSeed;
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }

        h ^= k.hashCode();

        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

1-3hashmap参数以及扩容机制

初始容量为 16, 达到阈值扩容, 阈值== 最大容量 * 负载因子(0.75),每次扩容后为原始容量的2倍。

扩容机制 resize()。 使用一个容量更大的数组来代替已有容量小的数组,transfer()方法将原有Entry数组的元素拷贝到新的Entry数组里,jdk1.7要重新计算元素在数组中的位置。 jdk1.8中不是重新计算而是采用了一种更巧妙的方式。

1-4get put源码1.7 1.8 都是没有枷锁

get put 1.8源码

 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; Node<K,V> first, e; int n; K k;
        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;
    }

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;
        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;
            else if (p instanceof TreeNode)
            	// 红黑树 插入 
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
            	// 链表 for查找 
                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;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;   // 对hashmap的修改次数 
        // 是否需要扩容 
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

1-5Java8对hashmap的改进

优化后的底层结构: 数组+ 链表 + 红黑树。当链表长度 >= 8时, 链表转变为红黑树,利用红黑树的插入,删除,查找的算法。

java8 中对hashmap扩容不是重新计算所有元素在数组的位置,而是我们使用的是2次幂的扩展(指长度扩为原来的2倍), 所以, 元素的位置要么是在原来的位置,要么是在原位置再移动2次幂的位置。不需要向jdk1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了。要是0的话索引就没变,要是1的话索引变为" 原索引 + oldCap "。

1-6hashmap为什么可以插入空值?

hashmap中添加 key =null, 的entry 会调用putForNullKey()直接通过遍历 table[0].entry链表
{
有key相同,更新value,返回oldValue。 不相同,就调用addEntry 添加一个key=null的entry
}

1-7hashmap为什么线程不安全?

扩容导致的

jdk1.7扩容时,多个线程造成环形链表,在get出造成死循环+ 数据丢失。
因为两个线程A, B 同时调用AddEntry。获取头结点,并构造插入会发生覆盖操作。

jdk1.8 并发操作put时,会发生数据覆盖的问题。

1-8hashmap中的key可以为任意对象或数据类型吗?

可以为null,但不能是可变对象,如果对象中属性改变,那么hashcode可变,导致下次无法找到数据。

如果可变对象能保证在不改变哈希值情况下,可以改变其属性值。

2-ConcurrentHashMap

2-1-jdk1.7下

2-1-1原理

ConcurrentHashMap1.7
一个ConcurrentHashMap 维护了一个Segment数组,一个Segment维护了一个hashEntry 数组。

其中Segment继承于 ReentrantLock
ConcurrentHashMap 使用分段锁技术,将数据分成一段段的存储,然后给每一段一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问,实现真正的并发访问。

我们来看看源码是怎样定义的,Segment
static final class Segment<K,V> extends ReentrantLock implements Serializable {

Segment 继承了ReentrantLock,表明每个Segment 都可以当做一个。这样对每个segment中的数据需要同步操作的话都是使用每个Segment容器对象自身的锁来实现。只有对全局需要改变时锁定的是所有的segment

2-1-2方法get,put ,remove

get()
   public V get(Object key) {
        Segment<K,V> s; // manually integrate access methods to reduce overhead
        HashEntry<K,V>[] tab;
        int h = hash(key);
        long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
        if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
            (tab = s.table) != null) {
            // 拿到指定的Segment ,遍历下面的hashentry节点下的 链表
            for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
                     (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
                 e != null; e = e.next) {
                K k;
                if ((k = e.key) == key || (e.hash == h && key.equals(k)))
                    return e.value;
            }
        }
        return null;
    }
put()
private int hash(Object k) {
     int h = hashSeed;

     if ((0 != h) && (k instanceof String)) {
         return sun.misc.Hashing.stringHash32((String) k);
     }

     h ^= k.hashCode();

     // Spread bits to regularize both segment and index locations,
     // using variant of single-word Wang/Jenkins hash.
     h += (h <<  15) ^ 0xffffcd7d;
     h ^= (h >>> 10);
     h += (h <<   3);
     h ^= (h >>>  6);
     h += (h <<   2) + (h << 14);
     return h ^ (h >>> 16);
 }

@SuppressWarnings("unchecked")
public V put(K key, V value) {
    Segment<K,V> s;
    if (value == null)
        throw new NullPointerException();
    int hash = hash(key);
    int j = (hash >>> segmentShift) & segmentMask;
    if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
         (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
        s = ensureSegment(j);
    return s.put(key, hash, value, false);
}
remove()
public V remove(Object key) {
    int hash = hash(key);
    Segment<K,V> s = segmentForHash(hash);
    return s == null ? null : s.remove(key, hash, null);
}

/**
 * {@inheritDoc}
 *
 * @throws NullPointerException if the specified key is null
 */
public boolean remove(Object key, Object value) {
    int hash = hash(key);
    Segment<K,V> s;
    return value != null && (s = segmentForHash(hash)) != null &&
        s.remove(key, hash, value) != null;
}
public int size() {
    // Try a few times to get accurate count. On failure due to
    // continuous async changes in table, resort to locking.
    final Segment<K,V>[] segments = this.segments;
    int size;
    boolean overflow; // true if size overflows 32 bits
    long sum;         // sum of modCounts
    long last = 0L;   // previous sum
    int retries = -1; // first iteration isn't retry
    try {
        for (;;) {
            if (retries++ == RETRIES_BEFORE_LOCK) {
                for (int j = 0; j < segments.length; ++j)
                    ensureSegment(j).lock(); // force creation
            }
            sum = 0L;
            size = 0;
            overflow = false;
            for (int j = 0; j < segments.length; ++j) {
                Segment<K,V> seg = segmentAt(segments, j);
                if (seg != null) {
                    sum += seg.modCount;
                    int c = seg.count;
                    if (c < 0 || (size += c) < 0)
                        overflow = true;
                }
            }
            if (sum == last)
                break;
            last = sum;
        }
    } finally {
        if (retries > RETRIES_BEFORE_LOCK) {
            for (int j = 0; j < segments.length; ++j)
                segmentAt(segments, j).unlock();
        }
    }
    return overflow ? Integer.MAX_VALUE : size;
}

2-2-jdk1.8及其以上

2-2-1原理

ConcurrentHashMap1.8

其中抛弃了原有的 Segment 分段锁,而采用了CAS + synchronized 来保证并发安全性。

大于 8 的时候才是链表转红黑树的阈值,当 table[i] 下面的链表长度大于·8时,就会转化为红黑树。

2-2-2-put操作--------重要哦

步骤:

根据 key 计算出 hashcode 。
判断是否需要进行初始化。
f 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。
如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。
如果都不满足,则利用 synchronized 锁写入数据(分为链表写入和红黑树写入)。
如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树。

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

/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
	// key 或者 value 抛出异常
    if (key == null || value == null) throw new NullPointerException();
    // 第一步,spread 会根据key的hashcode计算出hash值。
    int hash = spread(key.hashCode());
    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();
        // 第三部,定位到,当前的node,判断是否为空 为空则新建节点 
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        // 第四部,判断hash == MOVED 需要扩容了 
        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;
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            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;
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                       value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            if (binCount != 0) {
            //	链表 转化为 红黑树 因为 数量 >= 8
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    // 
    addCount(1L, binCount);
    return null;
}
spread
static final int spread(int h) {
    return (h ^ (h >>> 16)) & HASH_BITS;
}
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash

2-2-3get-size方法

根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。
如果是红黑树那就按照树的方式获取值。
就不满足那就按照链表的方式遍历获取值

public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
     int h = spread(key.hashCode());
     if ((tab = table) != null && (n = tab.length) > 0 &&
         (e = tabAt(tab, (n - 1) & h)) != null) {
         if ((eh = e.hash) == h) {
             if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                 return e.val;
         }
         else if (eh < 0)
             return (p = e.find(h, key)) != null ? p.val : null;
         while ((e = e.next
         ) != null) {
             if (e.hash == h &&
                 ((ek = e.key) == key || (ek != null && key.equals(ek))))
                 return e.val;
         }
     }
     return null;
 }
public int size() {
 long n = sumCount();
 return ((n < 0L) ? 0 :
         (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
         (int)n);
}

下一篇:
2-2 HashTable-LinkedHashMap-各个map的区别-hashset

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

下次遇见说你好

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值