In layman's language of Hashtable and HashMap + difference

Introduction: A few months ago thinking Spitzer telephone interview asked to HashMap, then ask is the difference between HashMap and HashTable today to look at HashMap in principle (with HashMap jdk1.8 full text of the object in question, the previous version is not do research, have time bloggers to add)

HashMap:

Underlying implementation:
Array + + list red-black tree
trunk HashMap is a Node array. Node HashMap is the basic unit, each Node comprises a key-value pairs. Also called a bucket (bucket), but the latter is more personal feeling some of the image.

1. Why is the list + red-black tree?

In jdk1.8 and later, when a bucket chain length greater than 8, the list structure is automatically converted into a red-black tree. The red-black tree search, insert, delete worst time complexity is O (log n), a single list of words is O (n). Mathematical Functions Figure
Here Insert Picture Description

2. why not start using a red-black tree?

If the chain length is 6 or less, although the time complexity is O (n), but this time to find speed quickly, and most importantly into spanning tree and will consume a certain time .
hashmap illustrated:
zZG4ubmV0L0FBQWh4eg==,size_16,color_FFFFFF,t_70)
converting red-black tree structure when the size of more than 8
Here Insert Picture Description

我们知道,一般解决哈希冲突的三种办法:
(1):开放定址法
(2):拉链法
(3):再散列法
当我们对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被其他元素占用了,其实这就是的哈希冲突,
那么HashMap采用的就是第二种方法,经计算得到的hash值相同的话放到一个“拉链”里
hash算法源码附上:

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

几个重要参数:下面也会用到这几个参数

    //默认起始容量
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    //最大扩容数量
    static final int MAXIMUM_CAPACITY = 1 << 30;
    //负载因子,代表了table的填充度有多少,默认是0.75,请注意看这一行
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    //这俩数就是上面提到的8和6,转换为树和链表的阈值(临界值)
    static final int TREEIFY_THRESHOLD = 8;
    static final int UNTREEIFY_THRESHOLD = 6;
    //最小树形化容量阈值
    static final int MIN_TREEIFY_CAPACITY = 64;

这里博主根据这几个值还发现了几个问题

3. 为什么负载因子(扩容因子)是0.75?

经过思考和一些参考,我们可以得出以下结论:
首先,我们上面提到了解决hash冲突的方法:拉链法,也就是在理想情况下,经过hash计算的每一个元素都会均匀地分布在每一个Node数组(bucket)里面,但是,假如我现在是0.75的扩容因子,先看以下源码里面的泊松分布的值

* 0:    0.60653066
* 1:    0.30326533
* 2:    0.07581633
* 3:    0.01263606
* 4:    0.00157952
* 5:    0.00015795
* 6:    0.00001316
* 7:    0.00000094
* 8:    0.00000006

那么就是当桶中元素到达8个的时候,概率已经变得非常小,每个碰撞位置的链表长度超过8个是几乎不可能的。因为越长的话操作起来越难。

那么假如我的扩容因子为0.95呢? 也就是平均20个桶里面只有1个是空的,那么就是这个代价是相当大的,就相当于是hash碰撞的特别特别厉害的时候才会出现这种情况,数组中的链表也就越容易长,而这种情况的出现会使get等操作效率大大降低!
那么假如负载因子是0.6或者更小呢? 你这个杠精,负载因子小不就扩容的次数越多吗?那扩容他不需要占用资源啊?过来挨打,所以选择0.75是一个这种的办法,而且是一种用空间换取时间的考虑。

4. 为什么会选择8作为阈值?

根据泊松分布,在负载因子默认为0.75的时候,通过泊松分布看出,当桶中结点个数为8时,出现的几率是亿分之6的(源码为我们算出来了),因此常见的情况是桶中个数小于8的情况,此时链表的查询性能和红黑树相差不多,而转化为树还需要时间和空间,所以此时没有转化成树的必要。

5. 为什么16是默认起始容量?

我的理解很鸡肋,就是这是一个经验值,即在这个值下既能保证碰撞的次数比较小,而又保证空间不被浪费。

6. 为什么hashmap的容量约定是2的倍数呢?

答:为了减少哈希碰撞的几率,选择了hash算法能让元素比较平衡的放到不同的桶中,而hash算法使用了位与&运算符。源码中使用了tab[i = (n - 1) & hash]。
当n=2时,n-1的二进制的后几位全是1,这时与操作更均匀。即更加均匀的让每一个bucket里面的size相同。

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;
        this.threshold = tableSizeFor(initialCapacity);
    }

好的,再往下,我们讨论一下HashMap的几个常见操作:
get

 public V get(Object key) {
        if (key == null)
            return getForNullKey();
        //计算hash值
        int hash = hash(key.hashCode());
        //先定位到数组元素,再遍历该元素处的链表
        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;
        }
        return null;
}

put源码放上之前,有必要说一个知识点就是:HashMap是非线程安全的
3. 问题3 为什么HashMap是非线程安全的? 这个非安全的原因无非是并发下的put扩容删除数据即对数据的操作造成的,下面先看源码:

非线程安全原因一:put

public V put(K key, V value) {
        //如果table数组为空数组{},进行数组填充(为table分配实际内存空间),入参为threshold,此时threshold为initialCapacity 默认是1<<4(24=16)
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
       //如果key为null,存储位置为table[0]或table[0]的冲突链上
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);//对key的hashcode进一步计算,确保散列均匀
        int i = indexFor(hash, table.length);//获取在table中的实际位置
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        //如果该对应数据已存在,执行覆盖操作。用新value替换旧value,并返回旧value
            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;
    }

我们知道:当发生 hash 冲突的时候,HashMap 是采用链表的方式来解决的,在对应的数组位置存放链表的头结点。对链表而言,新加入的节点会从头结点加入。
现在假如 A 线程和 B 线程两个线程同时进行插入操作,然后计算出了相同的哈希值对应了相同的数组位置,因为此时该位置还没数据,然后对一个数组同一个位置,两个线程会同时得到现在的头结点,然后 A 写入新的头结点之后,B 也写入新的头结点,那B的写入操作就会覆盖 A 的写入操作造成 A 的写入操作丢失,即put造成的非线程安全。

非线程安全原因二:扩容

说扩容之前先看几个重要参数:

    //默认起始容量
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    //最大扩容数量
    static final int MAXIMUM_CAPACITY = 1 << 30;
    //负载因子,代表了table的填充度有多少,默认是0.75,请注意看这一行
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    //这俩数就是上面提到的8和6,转换为树的阈值(临界值)
    static final int TREEIFY_THRESHOLD = 8;
    static final int UNTREEIFY_THRESHOLD = 6;
    //最小树形化容量阈值
    static final int MIN_TREEIFY_CAPACITY = 64;

扩容代码:

  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;
            }
            // 没超过最大值,就扩充为原来的2倍
            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;
    }

其中!多个线程同时操作,检测到总数量超过门限值的时候就会同时调用 resize 操作,各自生成新的数组并 rehash 后赋给该 map 底层的数组,结果最终只有最后一个线程生成的新数组被赋给该 map 底层,其他线程的均会丢失。A和B两个人同时对一个map进行扩容,A需要1000容量大小map在先,而B需要100大小的map,那么就会造成A的扩容结果失败。
这里有必要说一下就是:在jdk1.7的时候,HashMap解决hash冲突的时候采取的是头插法,这样在并发下,会造成

  1. 丢失数据
  2. 数组成环(假如有AB两个线程进行扩容,那么此时很容易造成:1->2,2->3,3->1的情况!)

非线程安全原因三:删除数据

源码:

    public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }
    final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        Node<K,V>[] tab; Node<K,V> p; int n, index;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
            Node<K,V> node = null, e; K k; V v;
            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<K,V>)p).getTreeNode(hash, key);
                else {
                    do {
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
                if (node instanceof TreeNode)
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                else if (node == p)
                    tab[index] = node.next;
                else
                    p.next = node.next;
                ++modCount;
                --size;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }

同上面两个操作,当多个线程同时操作同一个数组位置的时候,也都会先取得现在状态下该位置存储的头结点,然后各自去进行计算操作,之后再把结果写会到该数组位置去,其实写回的时候可能其他的线程已经就把这个位置给修改过了,就会覆盖其他线程的修改。
并发情况下要实现线程安全,可以采用:

  1. Hashtable
  2. 通过Collections.synchronizedMap()返回一个新的Map,这种方法底层源码上实现的是synchronize关键字+一个mutex即信号量,底层维护了一个用synchronize关键字加锁的Map
  3. ConcurrentHashMap

并发下应该用ConcurrentHashMap 摒弃Hashtable

因为HashTable操作十分繁重,每个线程,每个操作都用synchronize(悲观锁),以后博主会出一篇博客和大家一块研究一下ConcurrentHashMap ,其实主要的是jdk1.7ConcurrentHashMap用的是分段锁+volatile关键字来保持其内存可见性,而jdk1.8用的是CAS操作(乐观锁)+synchronize关键字。

HashMap&与Hashtable的区别

1. 作者不同

是不是很狗血但是就是作者不同啊

Hashtable:
Here Insert Picture Description
HashMap
Here Insert Picture Description

2. 是否符合驼峰命名法

很明显HashMap符合驼峰命名法,Hashtable不符合,我没有打错字!

3. 继承的父类不同

Hashtable

public class Hashtable<K,V>
extends Dictionary<K,V>
implements Map<K,V>, Cloneable, java.io.Serializable

HashMap

public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable

4. 初始化时机不同

Hashtable是在构造函数初始化,而HashMap是在第一次put()初始化hash数组。
Hashtable

//HashTable构造器 
public Hashtable(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal Load: "+loadFactor);
 
        if (initialCapacity==0)
            initialCapacity = 1;
        this.loadFactor = loadFactor;
        table = new Entry[initialCapacity];//初始化Hash数组
        threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
        initHashSeedAsNeeded(initialCapacity);
    }

HashMap

//hashMap的put函数 
public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);//初始化Hash数组
        }
        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;
    }

5. 默认大小和扩容方式不同

在HashTable中,hash数组默认大小是11,增加的方式是原来的2 + 1。在HashMap中,hash数组默认大小是16,增加的方式是2原来的而且一定是2的整数(这个在前面有说过)。

6. 是否允许非空键值

HashMap允许空键值,而HashTable不允许。所以我们在使用HashMap get到的键或者值为null的时候,不能判断该键值不存在!

7. hash值的使用不同

即计算数组下角标方式不同
Hashtable:

int hash = key.hashCode();
int index = (e.hash & 0x7FFFFFFF) % newCapacity;
//注意这里是直接调用的Object超类里面的hashCode

HashMap:

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

8. 内部方法不同

HashMap把Hashtable的contains()方法去掉了,改成了containsvalue()和containsKey()。
我就不列出代码了有点多了

9. 线程安全性不同

Hashtable的方法是线程安全的,而HashMap不支持线程的同步,不是线程安全的。

10. 迭代器不同

Hashtable使用Enumeration,HashMap使用Iterator。这个是快速失败的(fail-fast)还有一种失败方式是安全失败(fail-safe)值得一提的是java.util包下面的所有的集合类都是快速失败的,而java.util.concurrent包下面的所有的类都是安全失败的

  1. fail-fast: Fast Failure: When operating a plurality of threads, wherein if one iterator to traverse through the set of threads, the contents of the set are changed by another thread; ConcurrentModificationException exception will be thrown. The underlying maintains a number modCount, if not expected, then an error.
  2. fail-safe: Security Failure: The failure of the security mechanisms of the collection container, not directly access the contents of the collection while traversing, but the first copy of the original contents of the collection , to traverse on a copy of the collection. This does not directly throw an exception of

Fast failures and safety failures is iterator terms. Concurrent environment under recommended java.util.concurrent package containers, unless there is no modification operation.

Released nine original articles · won praise 73 · views 8736

Guess you like

Origin blog.csdn.net/AAAhxz/article/details/103777586