HashMap源码解析

HashMap

视频地址
哈希表的O(1)的平均查找、插入、删除时间

一、Java7 HashMap

在这里插入图片描述初始容量必须是2的指数次幂

默认初始容量:1<<4 = 16
左移一位表示乘2,左移4位表示乘以2的4次方。

默认的加载因子是0.75。
HashMap初始化数组不是在构造函数中创建数组的,而是在第一次调用put函数的时候。
如果初始化的时候大小不是2的幂,将向上调整成2的幂

1、put函数(没初始化的话,会在put函数中进行初始化)

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;
    }

每个元素的hashCode是从-2^ 31 到 + 2^31,可能是这些数字,如果取模放到数组中,有两个缺点:

  • 负数取余是负数,所以要把负数先变成正数
  • 速度较慢
hash函数(使得散列均匀)
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);
    }

hash函数可以保证分布均匀一点

indexFor函数源码(结果拿到数组索引(按位与得到))
static int indexFor(int h, int length) {
        // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
        return h & (length-1);
    }

2、为什么数组长度一定要是2的指数次幂(为了方便按位与得到索引)

求出hash函数求出hash值之后,源码中不是用的取模,而是使用hash值和数组长度减1进行按位与,hash&(length-1),因为length是2的次幂,所以length-1最后应该是都是1,比如15的二进制是0000 1111,所以得到的是前面都是0,后面都是1,这样和一个数进行按位与,前面还都是0,后面可能有0有1,但是一定小于等于length-1。

3、放入元素时执行的操作

如果hash值一样,key也一样,就会把值替换,返回,否则addEntry(超过容量会扩容size*加载因子,对所有元素重新计算hash)
在resize函数中进行重新计算hash值,放到新的位置。
扩容的容量会变成以前两倍。

4、链表节点Entry

HashMap底层好像就是用的这个数组

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;
        }

5、Java7的HashMap的问题

容易碰到死锁(多线程会碰到)

HashMap死锁问题

隐患(可能退化成链表,Hahs值都相同,如String类)

二、Java8 HashMap

1、Java8 HashMap的改进

在这里插入图片描述链表变成树的阈值为8。
红黑树的排序主要基于hashCode,如果两个元素的HashCode相同,且两个元素类相同和都实现了Comparable接口,则compareTo方法就会被用来排序。
TreeNode节点是普通节点(Entry?)的两倍大,所以在链表里面有足够多的元素的时候才使用它(根据TREEIFY_THRESHOLD?),当链表很小的时候,在你扩容或者resize的时候会转变成链表。在分布均匀的时候,红黑树代替链表用的比较少。
红黑树很少被用到, 理想情况下在随机HashCode的实现中, 桶(链表)中节点的个数服从泊松分布。桶中有1个、2个、3个元素…的概率如下:
在这里插入图片描述从上图可以看出,超过8的概率已经很小了。所以链表变成红黑树的阈值选择8.

put方法

public V put(K key, V value) 
{
    return putVal(hash(key), key, value, false, true);
}
hash函数(和Java7不同)
static final int hash(Object key) 
{
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); //异或,就是不进位的加法
}

putVal函数

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))))//如果hash相等,key相等,直接把值覆盖掉。
            e = p;
        else if (p instanceof TreeNode) //如果节点是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);
                    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;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

resize函数

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;
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    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;
    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)
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                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;
                        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;
}

Java8 HashMap不用rehash
e.hash & oldCap== 0
oldCap是2的指数次幂,所以只有最高位是1,这样就可以保证最高位按位与之后是0或者1,其他都是0,那么rehash之后要么还在原来位置,要么再加上oldCap。

JDK7resize中的transfer函数

void transfer(Entry[] newTable, boolean rehash) 
{
    int newCapacity = newTable.length;
    for (Entry<K,V> e : table) 
    {
        while(null != e) 
        {
            Entry<K,V> next = e.next;
            if (rehash) 
            {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            int i = indexFor(e.hash, newCapacity);
            e.next = newTable[i];
            newTable[i] = e;
            e = next;
        }
    }
}

源码中可能设计到的知识点

红黑树特性
  • 每个节点要么是红的要么是黑的
  • 根节点必须是黑色的
  • 每个叶子节点(指的是尾部NIL指针或者NULL节点 )都是黑的
  • 如果一个节点上红的,那么他的两个儿子都是黑的
  • 对任意节点而言,其到叶节点树尾端NIL指针的每条路径都包含相同的黑节点

在这里插入图片描述
插入6之后就会打破平衡,因为一个高度为2,一个为4,所以要旋转保持平衡。

原码

最高位为符号位,0代表正数,1代表负数,非符号位为该数字绝对值的二进制表示。

如:

127的原码为0111 1111
-127的原码为1111 1111

反码

正数的反码与原码一致;
负数的反码是对原码按位取反,只是最高位(符号位)不变。

如:

127的反码为0111 1111
-127的反码为1000 0000

补码

正数的补码与原码一致;
负数的补码是该数的反码加1。
如:

127的补码为0111 1111
-127的补码为1000 0001

ConcurrentHashMap

Segment (段)= 小HashMap
每段自带一把锁

  • 根据key算出对应的Segment是哪个
  • Segment.put

Java7 ConcurrentHashMap

视频地址

1、Segment构造函数

会传进来HashEntry数组,因为一个Segment包含一个HashEntry数组。

Segment(float lf, int threshold, HashEntry<K,V>[] tab)  
{
    this.loadFactor = lf;
    this.threshold = threshold;
    this.table = tab;
}

1、java7 Segment put方法(和HashMap的put差不多)

final V put(K key, int hash, V value, boolean onlyIfAbsent) 
{
    HashEntry<K,V> node = tryLock() ? null :
        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); //取出元素,在hashMap中是table[i],这样存在并发问题
        for (HashEntry<K,V> e = first;;) 
        {
            if (e != null) 
            {
                K k;
                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 {
                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;
}

2、HashEntry定义(和HashMap中的Entry差不多)

static final class HashEntry<K,V> 
{
    final int hash;
    final K key;
    volatile V value;
    volatile HashEntry<K,V> next;

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

为了确保读操作能够看到最新的值,将 value 设置成 volatile,这避免了加锁。

3、

默认并非级别16(也就是分几段)
最小Segment容量2

比如数组长16,有8个Segment,每个S有小的hashMap,HashMap.table.size=2;
会产生8个Segment数组,里面有HashMap即有Entry数组,大小为2。

如果分段树即并发级别(下面的ssize)传的不是2的指数次幂,将会转成2的指数次幂

ConcurrentHashMap构造函数

public ConcurrentHashMap(int initialCapacity,
                     float loadFactor, int concurrencyLevel) 
{
    if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
        throw new IllegalArgumentException();
    if (concurrencyLevel > MAX_SEGMENTS)
        concurrencyLevel = MAX_SEGMENTS;
    // Find power-of-two sizes best matching arguments
    int sshift = 0;
    int ssize = 1;  //就会变成Segment大小
    while (ssize < concurrencyLevel) {
        ++sshift;
        ssize <<= 1;
    }
    this.segmentShift = 32 - sshift;
    this.segmentMask = ssize - 1;
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    int c = initialCapacity / ssize;
    if (c * ssize < initialCapacity)
        ++c; //将c变为整数倍,最接近原来容量,比如原来initialCapacity是23,ssize是8,可以把c变成3。
    int cap = MIN_SEGMENT_TABLE_CAPACITY;
    while (cap < c)
        cap <<= 1;   //将cap变为最接近c的2的指数次幂
    // create segments and segments[0]
    Segment<K,V> s0 =
        new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
                         (HashEntry<K,V>[])new HashEntry[cap]);
    Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
    UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
    this.segments = ss;
}

上面的构造函数也就是把Segment的大小变成2的指数次幂,再把HashEntry的大小先变成Segment的倍数(向上取整),如果不是2的指数次幂,再将它转化成最近的2的指数次幂。

ConcurrentHashMap的put方法

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;  //找出应该放在哪个Segment下面去
    if ((s = (Segment<K,V>)UNSAFE.getObject(segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
        s = ensureSegment(j); //创建Segment
    return s.put(key, hash, value, false);
}

ensureSegment方法

private Segment<K,V> ensureSegment(int k) 
{
    final Segment<K,V>[] ss = this.segments;
    long u = (k << SSHIFT) + SBASE; // raw offset
    Segment<K,V> seg;
    if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) 
    {
        Segment<K,V> proto = ss[0]; // use segment 0 as prototype,Segment数组的第一个位置
        int cap = proto.table.length;//一个Segment包含的容量
        float lf = proto.loadFactor;
        int threshold = (int)(cap * lf);
        HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
        if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
            == null) { // recheck,双重检查
            Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
            while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                   == null) {
                if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
                    break;
            }
        }
    }
    return seg;
}

Segment[0]存放了一些数据,比如每个Segment的HashEntry数组的长度等

初始化好之后,在Segment的put函数的时候会进行加锁。
Segment继承了ReentrantLock类。

JDK8 ConcurrentHashMap

JDK8锁的不再是一个Segment,而是HashEntry中的一个对象。
锁的链表或者树的第一个节点

Unsafe使用及CAS的使用

import com.sun.scenario.effect.impl.sw.sse.SSEBlend_SRC_OUTPeer;
import sun.misc.Unsafe;

import java.lang.reflect.Field;


public class UnsafeTest
{
    private int i = 0;

    private static Unsafe unsafe = null;
    private static long I_OFFSET;  //i属性的偏移量
    static
    {
        try
        {
            // 只有bootstrap加载器加载的才能拿到Unsafe类,不然加载器是不一样的。
            // unsafe = Unsafe.getUnsafe();
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);
            //System.out.println(unsafe);
           // System.out.println(field);


            I_OFFSET = unsafe.objectFieldOffset(UnsafeTest.class.getDeclaredField("i"));
            //System.out.println(I_OFFSET);
        }
        catch (NoSuchFieldException | IllegalAccessException e)
        {
            e.printStackTrace();
        }

    }
    public static void main(String[] args)
    {
        final UnsafeTest unsafeTest = new UnsafeTest();
        new Thread(new Runnable() {
            @Override
            public void run()
            {
                while (true)
                {

                    boolean b = unsafe.compareAndSwapInt(unsafeTest, I_OFFSET, unsafeTest.i, unsafeTest.i+1);


                    //unsafeTest.i++;
                    if(b)
                    {
                        System.out.println(Thread.currentThread().getName()+":"+unsafe.getIntVolatile(unsafeTest,I_OFFSET));
                    }
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                }


            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run()
            {
                while (true) {

                    boolean b = unsafe.compareAndSwapInt(unsafeTest, I_OFFSET, unsafeTest.i, unsafeTest.i+1);

                    //System.out.println(b);
                    //unsafeTest.i++;
                    if(b)
                    {
                        System.out.println(Thread.currentThread().getName()+":"+unsafe.getIntVolatile(unsafeTest,I_OFFSET));
                    }
                    try
                    {
                        Thread.sleep(500);
                    }
                    catch (InterruptedException e)
                    {
                        e.printStackTrace();
                    }

                }


            }
        }).start();
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值