HashMap,HashTable,TreeMap,ConcurrentHashMap源码级总结!

最近面试面了不少了,总是被问到HashMap,有问HashMap和TreeMap区别的,有问HashMap和LinkedList区别的各种,反正就是排列组合。于是就搜集了一下,结合自己的理解,进行一下总结。如有错漏还请在评论区帮忙指出!

看总结直接拉到底。

 

还未了解的部分:各集合的迭代器。序列化接口部分。

 

hash值和hashCode:

Java令所有数据类型都继承了一个能够返回32比特整数hashCode()方法。

每一种数据类型的hashCode方法必须与equals方法一致。

hashCode与equals的关系不再详述了。

String的hashCode():

private final char value[];
/**
     * Returns a hash code for this string. The hash code for a
     * {@code String} object is computed as
     * 
     * s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
     * 
* using {@code int} arithmetic, where {@code s[i]} is the * ith character of the string, {@code n} is the length of * the string, and {@code ^} indicates exponentiation. * (The hash value of the empty string is zero.) * * @return a hash code value for this object. */ public int hashCode() { int h = hash; if (h == 0 && value.length > 0) { char val[] = value; for (int i = 0; i < value.length; i++) { h = 31 * h + val[i]; } hash = h; } return h; }

从注释里可以看到,String的hashCode返回的为:

s[0]*31^(n-1) + s[1]*31^[n-1] + ... + s[n-1]      (这里的^是乘方的意思,与后面的按位异或不要搞混)

例子:一个String s = "123";

它的hashCode为: '1' * 31^(3-1) + '2' * 31^(3-2) + '3' * 31^(3-3)

这里char需转为int。可以计算出hashCode为48690。

另外,可以去查看一下源码,会发现Integer的hashCode就是它的值。

为什么这样:

散列表希望hashCode()方法能够将键平均地散布为所有可能的32位整数。

取31。主要是因为31是一个奇质数,所以31i=32i-i=(i<<5)-i,这种位移与减法结合的计算相比一般的运算快很多。

但是在HashMap中并不是直接使用的hashCode()所产生的值来作为索引的。且看下段代码:

 /**
     * Computes key.hashCode() and spreads (XORs) higher bits of hash
     * to lower.  Because the table uses power-of-two masking, sets of
     * hashes that vary only in bits above the current mask will
     * always collide. (Among known examples are sets of Float keys
     * holding consecutive whole numbers in small tables.)  So we
     * apply a transform that spreads the impact of higher bits
     * downward. There is a tradeoff between speed, utility, and
     * quality of bit-spreading. Because many common sets of hashes
     * are already reasonably distributed (so don't benefit from
     * spreading), and because we use trees to handle large sets of
     * collisions in bins, we just XOR some shifted bits in the
     * cheapest possible way to reduce systematic lossage, as well as
     * to incorporate impact of the highest bits that would otherwise
     * never be used in index calculations because of table bounds.
     */
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

这段代码的意思是:(摘自)

hashmap中要找到某个元素,需要根据key的hash值来求得对应数组中的位置.
key的hash值高16位不变,低16位与高16位异或作为key的最终hash值。(h >>> 16,表示无符号右移16位,高位补0,任何数跟0异或都是其本身,因此key的hash值高16位不变。)

 

为什么要这样呢?连接里的文章有说,但我没看明白。思想大概就是使碰撞的概率最小。

其他的,比如HashTable里的hash计算方法是不一样的,具体看源码就行了。

 

HashMap:

从源码中可以看到,HashMap继承了AbstractMap类(一个抽象类),然后AbstractMap又实现了Map接口。

HashMap本身也有hashCode方法,是是现在AbstractMap里的,就是把所有Node的hashCode加起来了。

    public int hashCode() {
        int h = 0;
        Iterator> i = entrySet().iterator();
        while (i.hasNext())
            h += i.next().hashCode();
        return h;
    }

而Node的hashCode怎么计算呢?就是把key和value的hashCode按位异或一下。

        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

HashMap的MAXIMUM_CAPACITY 为 1<<30,也就是2的30次方。

HashMap的DEFAULT_LOAD_FACTOR为0.75。

HashMap的default initial capacity为16。扩容时候翻倍

HashMap的构造器有四种,分别是 默认,可以改initialCapacity,可以改loadFactor和initialCapactiy,在构造时直接创建一个Node。

HashMap的get(),containsKey()等,都用到了getNode ()方法。

/**
* The table, initialized on first use, and resized as
* necessary. When allocated, length is always a power of two.
* (We also tolerate length zero in some operations to allow
* bootstrapping mechanics that are currently not needed.)
*/
transient Node[] table;

final Node getNode(int hash, Object key) {
        Node[] tab; Node 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)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;
    }

具体为传入key的hash值,以及key本身。然后再getNode方法里找。这里的table暂且按下不表,这里只把table的注释挂上来,应该可以解释一二。

首先判断table是不是空的,也就是判断hashmap是不是空的,如果是空的就返回null好了。

然后这一行

(first = tab[(n - 1) & hash]) != null

说实话没有看明白,猜想应该是按位与找在数组里对应这个hash值的位置是否存有东西。如果这个不为null的话,就代表hash值至少是对上了,通过了数组这一关,可以去链表里找一找了。如果没有的话,就再hash值这一关被卡死了。

然后

if (first instanceof TreeNode)

只有有一行代码判断该节点是否为TreeNode。这是因为在java8里,如果链表的长度超过一定值(记得是8),就会把链表转为一个红黑树来储存。

 

然后就是put。hashMap的put是通过putVal方法来实现的。

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node[] tab; Node 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 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)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;
    }

首先,如果是第一次存入东西,就要开辟空间。

然后,如果新存入的key的hash值没有和原有的产生碰撞,就再table[(n - 1) & hash]位置新放一个Node,并存入新的键值对。

如果hash值碰撞了,就检测key是否已存在,检测结果有:单个键值对Node,链表,树。根据不同的结果做不同的事情。反正就是覆盖原始的键值对。

最后返回一个老的value。

 

上面看了一下HashMap常用方法的源码。接下来讲一下hashmap的特点。

1.Hash算法就是根据某个算法将一系列目标对象转换成地址,当要获取某个元素的时候,只需要将目标对象做相应的运算获得地址,直接获取。

2.线程不安全。

3.快。因为用了数组和链表,所以插入、删除、定位都很快。

4.每次扩容会重新计算key的hash值,消耗资源

5.如果key是自己的实现的,必须时间hashcode和euqels方法。

6.无序的,key不可重复,可以为null(只一个),vlaue可以重复和为null。.

 

 

Hashtable:

最大的不同点:这个Hashtable没有按照驼峰命名法!!

Hashtable继承的是Dictionary而Dictionary类里自己都说了这个类已经过时了,让人改用别的类。Hashtable自己也是快死的类了。没事别用它了。

* NOTE: This class is obsolete.  New implementations should
 * implement the Map interface, rather than extending this class.

initialCapacity 为11。扩容时候*2+1。

loadFactor为0.75。

它的hash计算法与HashMap里的不一样。

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

它的put的思想也是检测是否之前已经与相同的key了,如果有就更新,没有就新加一个。

但是,它的方法都是声明了synchronized的,也就是说,它是线程安全的。

public synchronized V put(K key, V value)
public synchronized V remove(Object key)
public synchronized void putAll(Map t) 
public synchronized void clear() 
public synchronized Object clone() 
....

一个疑问:

看了hashtable的源码之后,在put以及addEntry的代码里并没有找到它解决碰撞的方法。但看别人的文章说的是它解决碰撞的方法也是拉链法。还请懂的人在评论区告诉我一下。。

 

hashtable的特点:

1.线程安全的。自然也就慢了。

2.几乎被废弃的(父类都被废弃了)。

3.不允许null为key/value。

4.未遵循驼峰命名法。

5.重新计算hash、自己实现类作为key要重写hashcode equals、无序。这些跟hashmap是一样的。

 

TreeMap:

继承了AbstractMap抽象类。实现了NavigableMap接口。实现了Cloneable接口。实现了java.io.Serializable接口。

public interface NavigableMap extends SortedMap

这个接口的主要方法有:lowerEntry、floorEntry、ceilingEntry 和 higherEntry 分别返回与小于、小于等于、大于等于、大于给定键的键关联的 Map.Entry 对象,如果不存在这样的键,则返回 null。类似地,方法 lowerKey、floorKey、ceilingKey 和 higherKey 只返回关联的键。所有这些方法是为查找条目而不是遍历条目而设计的。摘自

也就是说,它支持一系列的导航方法,比如返回有序的key集合。

它可以被克隆,支持序列化。

TreeMap跟HashMap不一样,TreeMap用了红黑树来实现了。

TreeMap的基本操作 containsKey、get、put 和 remove 的时间复杂度是 log(n) 。

 

首先看一下TreeMap的构造函数:

    public TreeMap() {
        comparator = null;
    }

    public TreeMap(Comparator comparator) {
        this.comparator = comparator;
    }

    public TreeMap(Map m) {
        comparator = null;
        putAll(m);
    }

    public TreeMap(SortedMap m) {
        comparator = m.comparator();
        try {
            buildFromSorted(m.size(), m.entrySet().iterator(), null, null);
        } catch (java.io.IOException cannotHappen) {
        } catch (ClassNotFoundException cannotHappen) {
        }
    }

分别是无比较器,指定比较器,无比较器但是putAll一个map,以及指定比较器和一个SortedMap创建一个有着相同比较器和相同元素的map。

 

然后TreeMap的containsKey,get等方法都调用了getEntry方法来实现。

/**
     * Returns this map's entry for the given key, or {@code null} if the map
     * does not contain an entry for the key.
     *
     * @return this map's entry for the given key, or {@code null} if the map
     *         does not contain an entry for the key
     * @throws ClassCastException if the specified key cannot be compared
     *         with the keys currently in the map
     * @throws NullPointerException if the specified key is null
     *         and this map uses natural ordering, or its comparator
     *         does not permit null keys
     */
final Entry getEntry(Object key) {
        // Offload comparator-based version for sake of performance
        if (comparator != null)
            return getEntryUsingComparator(key);
        if (key == null)
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
            Comparable k = (Comparable) key;
        Entry p = root;
        while (p != null) {
            int cmp = k.compareTo(p.key);
            if (cmp < 0)
                p = p.left;
            else if (cmp > 0)
                p = p.right;
            else
                return p;
        }
        return null;
    }
/**
     * Version of getEntry using comparator. Split off from getEntry
     * for performance. (This is not worth doing for most methods,
     * that are less dependent on comparator performance, but is
     * worthwhile here.)
     */
    final Entry getEntryUsingComparator(Object key) {
        @SuppressWarnings("unchecked")
            K k = (K) key;
        Comparator cpr = comparator;
        if (cpr != null) {
            Entry p = root;
            while (p != null) {
                int cmp = cpr.compare(k, p.key);
                if (cmp < 0)
                    p = p.left;
                else if (cmp > 0)
                    p = p.right;
                else
                    return p;
            }
        }
        return null;
    }

首先判断comparator是否为null。差别就在与有没有指定比较器。

如果comparator不为null,调用另一个方法。如果comparator为null,就遍历树,查找有没有相符的key,如果没有相符的key就返回null。有就返回该Entry。

如果comparator不为null。调用getEntryUsingComparator(Object key)方法。就使用comparator的方式来遍历树并查找。

这里涉及到了comparator和comparable的差别。暂且按下不表。

 

TreeMap的put方法:典型的红黑树插入数据

public V put(K key, V value) {
        Entry t = root;
        if (t == null) {
            compare(key, key); // type (and possibly null) check

            root = new Entry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        int cmp;
        Entry parent;
        // split comparator and comparable paths
        Comparator cpr = comparator;
        if (cpr != null) {
            do {
                parent = t;
                cmp = cpr.compare(key, t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        else {
            if (key == null)
                throw new NullPointerException();
            @SuppressWarnings("unchecked")
                Comparable k = (Comparable) key;
            do {
                parent = t;
                cmp = k.compareTo(t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        Entry e = new Entry<>(key, value, parent);
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
        fixAfterInsertion(e);
        size++;
        modCount++;
        return null;
    }

首先,判断root是否为null,如果root为null,新加入的节点就是root了。并将modCount++(表示树被更改的次数);size++;

如果新插入的节点不是第一个,就要按照规则找地方插入。同样的,分为有没有指定comparator两种。这里用了if-else来区分。

如果找到了原先就在树里的key,就setValue来更新值。并返回。

如果key不在树里(直到找到的节点为null),则新建一个键值对,并将其放在树的新节点上。然后重排红黑树,按下不表,如需要深入了解红黑树且看另一篇文章

同样的,modCount和size要变化。

TreeMap的特点:

1.红黑树实现。有红黑树的优缺点。

2.红黑树中的key对象必须实现Comparable接口

3.不允许null key,但允许null value。

4.非线程安全。

5.无序(元素与添加顺序不一致),有序(键值大小有序。红黑树本身就是二叉查找树)

6.可以方便地实现排序、比较等方法。

7.TreeMap的增删改查和统计相关的操作的时间复杂度都为 O(logn)。比hashmap慢。

 

ConcurrentHashMap:

继承了AbstractMap接口,实现了Serializable接口。

这个与HashMap最大的区别就是,线程安全的。但是,怎么实现线程安全的呢?

Hashtable也是线程安全的,但是用的是synchronized,在加锁的时候会把整个都锁住。

但是ConcurrentHashMap只锁住Map的一部分,所以性能更好了。

ConcurrentHashMap由一个个的Segment组成,即为分段锁。ConcurrentHashMap即为一个Segment数组,Segment通过集成ReentrantLock来进行加锁。所以每次需要加锁的操作,锁住的为一个Segment。通过保证每个Segment的线程安全,来保证全局的线程安全。

Segment:

static class Segment extends ReentrantLock implements Serializable {
        private static final long serialVersionUID = 2249069246763182397L;
        final float loadFactor;
        Segment(float lf) { this.loadFactor = lf; }
    }

看不大懂,

长度为16,不可扩容。

来看看put过程。这里因为我也不大懂,第一次看,就摘抄一下

public V put(K key, V value) {
    Segment s;
    if (value == null)
        throw new NullPointerException();
    // 1. 计算 key 的 hash 值
    int hash = hash(key);
    // 2. 根据 hash 值找到 Segment 数组中的位置 j
    //    hash 是 32 位,无符号右移 segmentShift(28) 位,剩下低 4 位,
    //    然后和 segmentMask(15) 做一次与操作,也就是说 j 是 hash 值的最后 4 位,也就是槽的数组下标
    int j = (hash >>> segmentShift) & segmentMask;
    // 刚刚说了,初始化的时候初始化了 segment[0],但是其他位置还是 null,
    // ensureSegment(j) 对 segment[j] 进行初始化
    if ((s = (Segment)UNSAFE.getObject          // nonvolatile; recheck
         (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
        s = ensureSegment(j);
    // 3. 插入新值到 槽 s 中
    return s.put(key, hash, value, false);
}

通过hash来找到Segment。

然后第二层:

final V put(K key, int hash, V value, boolean onlyIfAbsent) {
    // 在往该 segment 写入前,需要先获取该 segment 的独占锁
    //    先看主流程,后面还会具体介绍这部分内容
    HashEntry node = tryLock() ? null :
        scanAndLockForPut(key, hash, value);
    V oldValue;
    try {
        // 这个是 segment 内部的数组
        HashEntry[] tab = table;
        // 再利用 hash 值,求应该放置的数组下标
        int index = (tab.length - 1) & hash;
        // first 是数组该位置处的链表的表头
        HashEntry first = entryAt(tab, index);
 
        // 下面这串 for 循环虽然很长,不过也很好理解,想想该位置没有任何元素和已经存在一个链表这两种情况
        for (HashEntry 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 {
                // node 到底是不是 null,这个要看获取锁的过程,不过和这里都没有关系。
                // 如果不为 null,那就直接将它设置为链表表头;如果是null,初始化并设置为链表表头。
                if (node != null)
                    node.setNext(first);
                else
                    node = new HashEntry(hash, key, value, first);
 
                int c = count + 1;
                // 如果超过了该 segment 的阈值,这个 segment 需要扩容
                if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                    rehash(node); // 扩容后面也会具体分析
                else
                    // 没有达到阈值,将 node 放到数组 tab 的 index 位置,
                    // 其实就是将新的节点设置成原链表的表头
                    setEntryAt(tab, index, node);
                ++modCount;
                count = c;
                oldValue = null;
                break;
            }
        }
    } finally {
        // 解锁
        unlock();
    }
    return oldValue;
}

在往segment里写之前,要先获取segment的独占锁。后面的操作和Hashmap的差不多,在修改完后,需要unlock。

初始化Segment:

private Segment ensureSegment(int k) {
    final Segment[] ss = this.segments;
    long u = (k << SSHIFT) + SBASE; // raw offset
    Segment seg;
    if ((seg = (Segment)UNSAFE.getObjectVolatile(ss, u)) == null) {
        // 这里看到为什么之前要初始化 segment[0] 了,
        // 使用当前 segment[0] 处的数组长度和负载因子来初始化 segment[k]
        // 为什么要用“当前”,因为 segment[0] 可能早就扩容过了
        Segment proto = ss[0];
        int cap = proto.table.length;
        float lf = proto.loadFactor;
        int threshold = (int)(cap * lf);
 
        // 初始化 segment[k] 内部的数组
        HashEntry[] tab = (HashEntry[])new HashEntry[cap];
        if ((seg = (Segment)UNSAFE.getObjectVolatile(ss, u))
            == null) { // 再次检查一遍该槽是否被其他线程初始化了。
 
            Segment s = new Segment(lf, threshold, tab);
            // 使用 while 循环,内部用 CAS,当前线程成功设值或其他线程成功设值后,退出
            while ((seg = (Segment)UNSAFE.getObjectVolatile(ss, u))
                   == null) {
                if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
                    break;
            }
        }
    }
    return seg;
}

获取写入锁: scanAndLockForPut

前面我们看到,在往某个 segment 中 put 的时候,首先会调用 node = tryLock() ? null : scanAndLockForPut(key, hash, value),也就是说先进行一次 tryLock() 快速获取该 segment 的独占锁,如果失败,那么进入到 scanAndLockForPut 这个方法来获取锁。

下面我们来具体分析这个方法中是怎么控制加锁的。

private HashEntry scanAndLockForPut(K key, int hash, V value) {
    HashEntry first = entryForHash(this, hash);
    HashEntry e = first;
    HashEntry node = null;
    int retries = -1; // negative while locating node
 
    // 循环获取锁
    while (!tryLock()) {
        HashEntry f; // to recheck first below
        if (retries < 0) {
            if (e == null) {
                if (node == null) // speculatively create node
                    // 进到这里说明数组该位置的链表是空的,没有任何元素
                    // 当然,进到这里的另一个原因是 tryLock() 失败,所以该槽存在并发,不一定是该位置
                    node = new HashEntry(hash, key, value, null);
                retries = 0;
            }
            else if (key.equals(e.key))
                retries = 0;
            else
                // 顺着链表往下走
                e = e.next;
        }
        // 重试次数如果超过 MAX_SCAN_RETRIES(单核1多核64),那么不抢了,进入到阻塞队列等待锁
        //    lock() 是阻塞方法,直到获取锁后返回
        else if (++retries > MAX_SCAN_RETRIES) {
            lock();
            break;
        }
        else if ((retries & 1) == 0 &&
                 // 这个时候是有大问题了,那就是有新的元素进到了链表,成为了新的表头
                 //     所以这边的策略是,相当于重新走一遍这个 scanAndLockForPut 方法
                 (f = entryForHash(this, hash)) != first) {
            e = first = f; // re-traverse if entry changed
            retries = -1;
        }
    }
    return node;
}

这个方法有两个出口,一个是 tryLock() 成功了,循环终止,另一个就是重试次数超过了 MAX_SCAN_RETRIES,进到 lock() 方法,此方法会阻塞等待,直到成功拿到独占锁。

这个方法就是看似复杂,但是其实就是做了一件事,那就是获取该 segment 的独占锁,如果需要的话顺便实例化了一下 node。

以上大段摘抄了

 

总结:总结ConcurrentHashMap就是一个分段的hashtable ,根据自定的hashcode算法生成的对象来获取对应hashcode的分段块进行加锁,不用整体加锁,提高了效率。.

 

 

 

总之:

1.HashMap、Hashtable、ConcurrentHashMap用了散列表,而TreeMap用了红黑树。

2.Hashtable已被废弃,用ConcurrentHashMap来代替它。前者是对整个map加锁,而后者是分段锁。所以性能更好。

3.HashMap因为不用考虑加锁,所以性能比线程安全的两个更好(思考:如何使HashMap线程安全?)。

4.因为链表、红黑树的特点。HashMap插入和查询和更改的时间复杂度都是O(1),删除的话应该也是O(1)。TreeMap都是O(logN)。

5.TreeMap里的元素顺序与插入顺序无关,但是很方便就能实现按key排序。

6.各种集合的key和value对null值的容忍也是不同的。

7.以散列表实现的,都需要对象key类有hashCode()和equals()方法。以红黑树实现对象key类的需要实现Comparable接口。

 

 

 

你可能感兴趣的:(Java)