zoukankan      html  css  js  c++  java
  • HashTable 源码解读

    很多人都知道HashTable与HashMap的关系,HashTable是线程安全的,HashMap是非线程安全的。在介绍完HashMap之后,趁热介绍一下HashTable。在HashTable中没有像HashMap中那么多关于数据结构的内容。HashTable是线程安全的,因为其源码的方法里都带有synchronized,但是效率不高,如果想使用高性能的Hash结构,建议使用java.util.concurrent.ConcurrentHashMap

    HashTable 存储的数据类型

    HashTable的key和value都可以为空,在存储的过程中 key必须实现 hashCode()和equals()两个方法。

    影响HashTable性能的两个参数

    HashTable中的两个变量影响其性能:初始容量与负载系数(load factor)。

    容量

    指的时hashtable中桶的个数。桶其实就是单向的链表。hashtable 是允许hash 冲突的,单个桶(链表)可以存储多个entry。在定义HashTable的初始容量的大小时,要权衡是空间 和 重新hash运算(很耗时)之间的利弊。当初始的容量大于元素的最大个数时,将不会发生rehash运算,但是太大的初始容量意味着浪费了很多空间。如果能提前估算出要向hashTable中存很多值时,就要给一个适合的初始容量,因为在添加数据时如,果不需要rehash操作的话将会更快。

    负载系数

    指的是hashtable在自动扩容之前允许桶多满?默认的负载系数为0.75,增大可以减少每次扩容的大小,但是增加了查找所花费的时间。

    数据结构

    前面也提到了,HashTable内部存储了一个table数组,这个数组的每一个元素存储的都是链表的头。在存储数值时,定位存储位置是通过如下代码:

        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length; // 去掉符号位的影响
        Entry<K,V> e = (Entry<K,V>)tab[index];
    

    上面的代码就确定了当前key的节点位于哪个链表上,e 即链表头。如果在该链表中无法找到对应的key,则将当前的节点添加到链表的头部。

            Entry<K,V> e = (Entry<K,V>) tab[index];
            // 把链表的头部传进去,为了将new 出来的节点.next指向原来链表的头部
            tab[index] = new Entry<>(hash, key, value, e); 
    

    rehash算法

    rehash算法,也可以理解为扩容算法,当table装不下要存储的值的时候,这是后就需要扩容增加内部数组的长度,这下惨了,每个key存储到哪个链表中是和table.length有直接关系的,所以在扩容时,要把当前hashtable中存储的节点重新计算一遍存储位置,这就是前面提到的为什么rehash会很耗时。

    protected void rehash() {
        int oldCapacity = table.length;
        Entry<?,?>[] oldMap = table;
    
        // overflow-conscious code
        int newCapacity = (oldCapacity << 1) + 1; // 容量每次扩大一倍
        if (newCapacity - MAX_ARRAY_SIZE > 0) {
            if (oldCapacity == MAX_ARRAY_SIZE)
                // Keep running with MAX_ARRAY_SIZE buckets
                return;
            newCapacity = MAX_ARRAY_SIZE;
        }
        Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
    
        modCount++; //结构变化
        threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
        table = newMap;
    
        for (int i = oldCapacity ; i-- > 0 ;) {
            for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
                Entry<K,V> e = old; // 将当前的变量赋值给暂存变量
                old = old.next; // 继续获取链表的下一个节点,为下一次循环做准备
    
                int index = (e.hash & 0x7FFFFFFF) % newCapacity; // 计算当前节点在newMap中存储的位置
                // 每次插入数据都插入到链表的头
                e.next = (Entry<K,V>)newMap[index];  // 将当前节点的指针指向原来链表的头
                newMap[index] = e; // 将当且节点存入数组中
            }
        }
    }
    

    compute方法

    这不是hashMap独有的,是Map接口定义的。放在这里讲的原因是:HashTable没什么好写的,正好从HashMap把这部分内容搬过来。

    computeIfAbsent,computeIfPresent,compute 三个方法,这三个方法本质上都是根据给定的key更新当前map中的值,HashMap中也有同样的方法
    下面是一个简短的例子

    public static void main(String[] args) {
    HashMap<String,Integer> map = new HashMap<>();
    for (int i = 0; i< 10; i++) {
    map.put(String.valueOf(i),i);
    }
    map.computeIfPresent(String.valueOf(5),new MyFunction()); // 如果存在则计算
    System.out.println(map);
    
    map.computeIfAbsent(String.valueOf(20),new AbsentFunciton());  //如果不存在添加
    System.out.println(map);
    
    map.compute(String.valueOf(8),new MyFunction());    //如果存在则计算,不存在添加
    System.out.println(map);
    }
    
    //上面要使用的接口实现
    class MyFunction implements BiFunction {
    
        @Override
        public Object apply(Object key, Object oldValue) {
            if (key.equals("5")) {
                return (Integer)oldValue + 3;
            }
            return oldValue;
        }
    }
    
    class AbsentFunciton implements Function{
    
        @Override
        public Object apply(Object key) {
            return key;
        }
    }
    

    下面对3个方法进行一下介绍

    computeIfAbsent

    根据给定的key 在hashtable中查找,如果找到了返回key对应的值,如果没找到,根据定义的计算功能,算出新值,如果新值不为空,添加到hashtable中
    
    public synchronized V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
        //计算功能不能为空
        Objects.requireNonNull(mappingFunction);
        // 缓存内部table
        Entry<?,?> tab[] = table;
        // 根据给定的key 计算出hash
        int hash = key.hashCode();
        // 根据hash求出在数组第几个链上
        int index = (hash & 0x7FFFFFFF) % tab.length;
        @SuppressWarnings("unchecked")
        Entry<K,V> e = (Entry<K,V>)tab[index];
        // 如果在链表中找到,则返回旧值
        for (; e != null; e = e.next) {
            if (e.hash == hash && e.key.equals(key)) {
                // Hashtable not accept null value
                return e.value;
            }
        }
        // 记录modCount 在计算时,不允许修改hashtable结构
        int mc = modCount;
        // 获得根据计算功能计算出的新值
        V newValue = mappingFunction.apply(key);
        if (mc != modCount) { throw new ConcurrentModificationException(); }
        // 如果新值不为空,添加到hashtable中
        if (newValue != null) {
            addEntry(hash, key, newValue, index);
        }
        // 返回新值
        return newValue;
    }
    

    computeIfPresent

    根据给定的key在hashtable中查找,如果没找到,返回空,如果找到了,根据定义的功能,计算出新值,如果新值为 null,则将key对应的节点删除,如果不是空,更新节点值,最后返回新值。
    
    public synchronized V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
        //定义的计算功能不能为空
        Objects.requireNonNull(remappingFunction);
        //复制一份table
        Entry<?,?> tab[] = table;
        // 根据hash计算在hashtable中的位置
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        @SuppressWarnings("unchecked")
        Entry<K,V> e = (Entry<K,V>)tab[index];
        // 在对应的位置的链表中查找
        for (Entry<K,V> prev = null; e != null; prev = e, e = e.next) {
            if (e.hash == hash && e.key.equals(key)) {
                // 如果找到了,根据key、旧值和定义的功能计算出新值 
                int mc = modCount;
                V newValue = remappingFunction.apply(key, e.value);
                if (mc != modCount) {
                    throw new ConcurrentModificationException();
                }
                // 要将新值赋值到原来的key上,如果新值为空,则要在链表上删除对应的节点,计数器-1
                if (newValue == null) {
                    if (prev != null) {
                        prev.next = e.next; 
                    } else {
                        tab[index] = e.next;
                    }
                    modCount = mc + 1;
                    count--;
                } else {
                    e.value = newValue;
                }
                // 返回新值
                return newValue;
            }
        }
        // 如果在对应位置上的链表中没有找到,则返回空
        return null;
    }
    

    compute

    思路就是有就更新,没有就添加,是上面两个的整合。
    
    public synchronized V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
        // 定义的计算功能不能为空
        Objects.requireNonNull(remappingFunction);
    
        Entry<?,?> tab[] = table;
        // 根据hash获取链表位置
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        @SuppressWarnings("unchecked")
        Entry<K,V> e = (Entry<K,V>)tab[index];
        如果根据key找到了对应的节点,更新对应的节点值,并返回根据功能计算的新值
        for (Entry<K,V> prev = null; e != null; prev = e, e = e.next) {
            if (e.hash == hash && Objects.equals(e.key, key)) {
                int mc = modCount;
                V newValue = remappingFunction.apply(key, e.value);
                if (mc != modCount) {
                    throw new ConcurrentModificationException();
                }
                if (newValue == null) {
                    if (prev != null) {
                        prev.next = e.next;
                    } else {
                        tab[index] = e.next;
                    }
                    modCount = mc + 1;
                    count--;
                } else {
                    e.value = newValue;
                }
                return newValue;
            }
        }
        // 如果没有找到根据key 计算出新值,如果新值不为空添加到table中,返回计算的新值
        int mc = modCount;
        V newValue = remappingFunction.apply(key, null);
        if (mc != modCount) { throw new ConcurrentModificationException(); }
        if (newValue != null) {
            addEntry(hash, key, newValue, index);
        }
    
        return newValue;
    }
    
    I am chris, and what about you?
  • 相关阅读:
    HTTP 错误 404.13
    C# 文件操作(全部) 追加、拷贝、删除、移动文件、创建目录 修改文件名、文件夹名
    设计模式---装饰模式(Decorator)
    设计模式---订阅发布模式(Subscribe/Publish)
    Merge into 详细介绍
    优化案例--多语句表值函数的影响
    常用脚本--Kill所有连接到指定数据库上的回话
    常用脚本--查看当前锁信息
    常用脚本--查看死锁和阻塞usp_who_lock
    常用脚本--在线重建或重整实例下所有索引
  • 原文地址:https://www.cnblogs.com/arax/p/8206702.html
Copyright © 2011-2022 走看看