jdk源码解析(一)-hashtable和hashmap在jdk源码中的演进

对比学习法:
知道A是A,B是B,记不住,但是知道A和B有不同点C,然后导致A和B的表现差异D。这样可以学会因为C,导致D。

本文主要对比两个问题
1.hashmap和hashtable
2.hashmap在jdk1.6,1.8 这2个版本的源码实现差异
(1.2是一版,但是找不到源码了,1.6-1.7是一版,1.8又改了一部分) 改动不大,但是有区别

什么是散列表

前置知识

数据结构的目的: 提高效率
我们对数据一般有4个操作 (增删改查),其实就是2个基本操作 增和查

  • 增 (顺序增加(头尾插入),随机插入)
  • 删 (本质是查后删)
  • 改 (本质是查后改)
  • 查 (这个是难点,一般来讲,存入数据后,主要是查,按结构不同效率不同)

算法的复杂度一般单次操作以
O(1)最优,O(log(n))其次,O(n)最慢

对比常用数据结构的 增删查 时间复杂度
数组:

  • 增 插入O(1)
  • 删 查找O(1)
  • 查 查找O(1)
  • 扩容: 数据容量固定,扩容的代价为全部重新插入 O(n)

链表:

  • 增 头尾插入O(1),指定位置插入 需要先查O(n),再插入
  • 删 先查O(n) 再删 O(1)
  • 查 O(n)
  • 扩容: 无容量限制

可以查找的树是有序树,例如 二叉搜索树,红黑树,按红黑树来说
红黑树:

  • 增 O(log(n))
  • 删 先查O(log(n)),再删O(1)
  • 查 O(log(n))
  • 扩容: 链表实现无容量限制

最理想的增删查效率为

  • 增 O(1)
  • 删 先查O(1)
  • 查 O(1)
  • 扩容: 不用扩容或自动扩容

散列表就是一种结合数组+链表的数据结构,在限定范围内,效率可以达到上述的 理想效率
散列表的效率:
定位靠数组, 增删查靠链表,利用多链表,减少每一个链表的长度,最理想状态是,链表长度 = 1,实现理想效率,假设数组长度为N,链表长度为n,n的理想状态为1

  • 增 定位O(1) 头插 O(1)
  • 查 定位O(1) 查找 O(n)
  • 删 先查O(n) 删除O(1)
  • 链表扩容和冲突: 无容量限制,冲突就追加链表
  • 数组扩容: 为了降低链表长度,保证查询效率,数组会扩容,重散列 O(N)

那么在什么情况下,散列表的效率最优
避免掉所有O(n)的项
1.数组不用扩容 避免O(N)的扩容
2.散列非常均匀,每个链表的长度为1
增,查,删,都是O(1) 理想效率

在什么情况下,散列表的效率最差
1.数组散列极度不均,链表长度为N
2.频繁扩容 O(N)
增 O(1) 查O(N)

散列表的具体实现就不详细说了,资料网上很多
下面看源码解析

主要从四个角度查看区别:

  1. hash()方法的实现,怎么保证散列均匀 (如何降低链表长度)
  2. 链表实现,为什么这么实现,如何优化查询效率
  3. 自动扩容机制,优化空间
  4. 线程安全

Hashtable jdk 1.0

1.hash方法

int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;

对 length 求余得 index

2.链表实现

private static class Entry<K,V> implements Map.Entry<K,V> {
    int hash;
    K key;
    V value;
    Entry<K,V> next;
    // ....
}

链表实现是 普通的单链表

for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
    if ((e.hash == hash) && e.key.equals(key)) {
        return e.value;
    }
}

查询方式:顺序遍历链表
查询效率: O(n)

3.自动扩容

public Hashtable() {
    this(11, 0.75f);
}

初始容量 11 , 阈值0.75,

// 扩容机制
int newCapacity = oldCapacity * 2 + 1;
// rehash机制
// 创建新的数组,遍历所有节点,rehash,
// 重新构建新的链表 假设插入速度O(1),则扩容效率O(N)

自动扩容 2N+1

4.线程安全
hashtable的方法由 synchronized 修饰 线程安全
但竞争会导致严重的效率低下,例如: 对不同key的put和get会相互阻塞,但是实际这里并没有竞争关系

HashMap jdk 1.6 ~ 1.7

1.hash方法

int hash = (key == null) ? 0 : hash(key.hashCode());
static int hash(int h) {
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}
static int indexFor(int h, int length) {
    return h & (length-1);
}

后面综合比较效率,这里暂时不对比hashtable

2.链表实现

static class Entry<K,V> implements Map.Entry<K,V> {
    final K key;
    V value;
    Entry<K,V> next;
    final int hash;
}

链表结构没变化

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.equals(k)))
        return e.value;
}

遍历方式没有改变, 查找效率O(n)

3.自动扩容

public HashMap() {
	// static final float DEFAULT_LOAD_FACTOR = 0.75f
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    // static final int DEFAULT_INITIAL_CAPACITY = 16;
    threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
}

默认容量为 16, 阈值为 0.75f

这里有一个优化点:
如果手动指定任意长度为容量, 则会将容量调整为 最接近的2的次方的值

public HashMap(int initialCapacity, float loadFactor) {
    // Find a power of 2 >= initialCapacity
    int capacity = 1;
    while (capacity < initialCapacity)
        capacity <<= 1;
}

问题: 为什么容量一定要为2的次方,hashtable就不需要。

解答:
我们看indexFor方法 正常思路是 hash % length 得到index
indexFor怎么做的呢? hash & (length - 1) ,如果要 hash & (length - 1) = hash % length 则 length 必须是2的次方
然后 hash & (length - 1) 效率 大于 hash % length
注: 后面有测试结果

扩容算法

int newCapacity = oldCapacity * 2 + 1;
void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }

    Entry[] newTable = new Entry[newCapacity];
    transfer(newTable);
    table = newTable;
    threshold = (int)(newCapacity * loadFactor);
}

/**
 * Transfers all entries from current table to newTable.
 */
void transfer(Entry[] newTable) {
    Entry[] src = table;
    int newCapacity = newTable.length;
    for (int j = 0; j < src.length; j++) {
        Entry<K,V> e = src[j];
        if (e != null) {
            src[j] = null;
            do {
                Entry<K,V> next = e.next;
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            } while (e != null);
        }
    }
}

初始容量 11 , 阈值0.75, 自动扩容 2N+1
4.线程安全
线程不安全
1.modCount是共享变量,线程不安全
2.put时,线程A往节点N1后面增加节点,同时,线程B往节点N1后面增加节点B1成功,理论上,B1->N1链形成,A应该添加到B1前面,但依然添加到N1前面,导致B1丢失。
3.resize时会导致循环链表,get时陷入死循环
多个线程同时进行rehash操作,关键在于transfer(),这个方法的作用是将旧hash表中的元素rehash到新的hash表中,当多线程同时修改同一个节点的next时,会导致循环链表出现

HashMap jdk 1.8

1.hash方法

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// index求法 n表示长度length
i = (n - 1) & hash

这里比1.6优化了
求hash的步骤从 4步变为2步 更快了

2.链表实现

static class Node<K,V> implements Map.Entry<K,V> {
    final K key;
    V value;
    Entry<K,V> next;
    final int hash;
}

链表结构没变化

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

这一段get变化很大,是因为1.8以后,hashMap对链表做了优化:

  • 1.链表长度>8时,转化为红黑树 优化查询速度 O(n)->O(log(n))
  • 2.红黑树节点少于6时,转化为链表

3.自动扩容

public HashMap() {
	// static final float DEFAULT_LOAD_FACTOR = 0.75f
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    // static final int DEFAULT_INITIAL_CAPACITY = 16;
    threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
}

默认容量为 16, 阈值为 0.75f

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

这个比1.6的循环直到找到大于capacity的2的次方为止的算法要效率高

扩容算法

int newCapacity = oldCapacity * 2 + 1;
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;
}

初始容量 11 , 阈值0.75, 自动扩容 2N+1 和1.6的相同

4.线程安全
线程不安全
存在多线程put导致节点丢失的线程安全问题
但是1.8不存在多线程resize导致get死循环的情况了

性能对比:

1.hash算法效率对比

public class HashMapSpeed {

    public static void hashtable(int num,int length) {
        long st = System.currentTimeMillis();
        long sum = 0;
        for(int i = 0 ; i < num ; i++) {
            int hash = Integer.hashCode(i);
            sum +=  (hash & 0x7FFFFFFF) % length;
        }
        long et = System.currentTimeMillis();
        System.out.println("sum:"+sum+",time:"+(et-st));
    }

    public static void hashMap16(Integer num,int length) {
        long st = System.currentTimeMillis();
        long sum = 0;
        for(int i = 0 ; i < num ; i++) {
            int h = Integer.hashCode(i);
            h ^= (h >>> 20) ^ (h >>> 12);
            h ^= (h >>> 7) ^ (h >>> 4);
            sum += h & (length - 1);
        }
        long et = System.currentTimeMillis();
        System.out.println("sum:"+sum+",time:"+(et-st));
    }

    public static void hashMap18(Integer num,int length) {
        long st = System.currentTimeMillis();
        long sum = 0;
        for(int i = 0 ; i < num ; i++) {
            int h = Integer.hashCode(i);
            h ^= (h >>> 16);
            sum += h & (length - 1);
        }
        long et = System.currentTimeMillis();
        System.out.println("sum:"+sum+",time:"+(et-st));
    }

    public static void main(String[] args) {
        //
        int num = 100000000;
        hashtable(10000000,256);
        hashMap16(10000000,256);
        hashMap18(10000000,256);
    }
}

结果

sum:1274991808,time:83
sum:1274991808,time:49
sum:1275008192,time:34

hash 一亿次,假设长度为256,效率对比如上图。

总结

hashtable
线程安全:通过synchorized实现 效率较低
普通散列表实现,会存在链表长度过长的情况
初始长度为11

hashmap: 线程不安全
想要线程安全请用ConcurrentHashMap
长度设置为 2的倍数,是为了优化hash的速度
从1.8开始,链表长度超过8时,转化为红黑树,低于6时,转化为链表
初始长度为16

hashmap连问

1.为什么用hashmap不用hashtable

hashtable的效率不如hashmap,
hashtable的key,value不支持null

2.hashmap的工作原理是什么

散列表数据结构,

3.HashMap的put过程

jdk 1.8:
1.hash后计算下标
2.找到对应的链表后,查找key值是否存在,
查找过程分为: 链表遍历或红黑树查找
找到以后,修改value值
否则,头插入新节点,
3.判断链表长度是否达到8,转化为红黑树
4.判断是否需要resize(),进行resize();

4.HashMap的get过程

1.hash后计算下标
2.找到对应的链表,查找key是否存在
	如果节点是链表节点,遍历查找
	如果节点是TreeNode,红黑树节点,二叉搜索树

5.怎么减少碰撞,即hash更散列

从HashMap的角度是: 更好的hash算法
从用户的角度: 更好的HashCode生成,不同的hashcode散列到不同的桶中的概率更低。相同的hashcode则会散列到同样的桶中。

6.HashMap的hash()方法怎么实现的

1.6的
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);

1.8的
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
扰动次数更少,效率更高

7.HashMap的链表为什么用红黑树,不用二叉数,为什么需要8以上用红黑树,为什么不一直用红黑树?

二叉树在数据有序情况下可能变成一条链表,等于没有优化。
红黑树的插入比链表更加繁琐,为了保证插入和查找的高效,优先使用链表,
只有当链表长度过长时,才会转化为红黑树,均衡插入和查找的效率

8.说说红黑树

咳咳。。。

9.解决碰撞的其他办法?

1.链表法,HashMap这样插入链表的办法
2.开放寻址法,往数组后面的空位存放
3.再寻址法,再次hash,直到找到空位

10.如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?

会进行rehash,数组大小*2,遍历所有节点,重新进行插入

11.重新调整HashMap大小存在什么问题吗?

jdk 1.6~1.7
get无限循环问题
多个线程同时进行rehash,因为插入方式为头插入,会导致A插入的节点以后,B重新将其头插入,形成循环链路,
导致get时无限循环
多线程put导致节点丢失
jdk1.8
修复了get无限循环的问题
还存在多线程put导致节点丢失问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值