HashMap、ConcurrentHashMap

HashMap的实现原理(JDK1.7):

一. 数据结构

public interface Map<K,V> {

   interface Entry<K,V> {

   K getKey();

   V getValue();

   ... ...

   }

}
public class HashMap<K,V> extends AbstractMap<K,V>{

  static class Node<K,V> implements Map.Entry<K,V> {

        final int hash;

        final K key;

        V value;

        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {

            this.hash = hash;

            this.key = key;

            this.value = value;

            this.next = next;

        }

        public final K getKey()        { return key; }

        public final V getValue()      { return value; }

        public final String toString() { return key + "=" + value; }

  }
}

HashMap中的Node<K,V> 实现了Map.Entry,将每个key-value对维护在其中,并将全部的key-value对维护在Node<K,V>[] table数组中。html

二.方法

  1. put方法:
public V put(K key, V value) 
 { 
	 // 若是 key 为 null,调用 putForNullKey 方法进行处理
	 if (key == null) 
		 return putForNullKey(value); 
	 // 根据 key 的 keyCode 计算 Hash 值
	 int hash = hash(key.hashCode()); 
	 // 搜索指定 hash 值在对应 table 中的索引
 	 int i = indexFor(hash, table.length);
	 // 若是 i 索引处的 Entry 不为 null,经过循环不断遍历 e 元素的下一个元素
	 for (Entry<K,V> e = table[i]; e != null; e = e.next) 
	 { 
		 Object k; 
		 // 找到指定 key 与须要放入的 key 相等(hash 值相同
		 // 经过 equals 比较放回 true),则覆盖原Entry
		 if (e.hash == hash && ((k = e.key) == key 
			 || key.equals(k))) 
		 { 
			 V oldValue = e.value; 
			 e.value = value; 
			 e.recordAccess(this); 
			 return oldValue; 
		 } 
	 } 
	 // 若是 i 索引处的 Entry 为 null,代表此处尚未 Entry ,将新的Entry放在此处
     //又若是,hashCode相等,但equals返回,则将新的Entry与旧的Entry放在一块儿造成链
	 modCount++; 
	 // 将 key、value 添加到 i 索引处
	 addEntry(hash, key, value, i); 
	 return null; 
 }

当向 HashMap 中添加 key-value 对,由其 key 的 hashCode() 返回值决定该 key-value 对(就是 Entry 对象)的存储位置。当两个 Entry 对象的 key 的 hashCode() 返回值相同时,将由 key 经过 eqauls() 比较值决定是采用覆盖行为(返回 true),仍是产生 Entry 链(返回 false)。这就是解决hash冲突的方法。java

void addEntry(int hash, K key, V value, int bucketIndex) 
{ 
    // 获取指定 bucketIndex 索引处的 Entry 
    Entry<K,V> e = table[bucketIndex]; 	 // ①
    // 将新建立的 Entry 放入 bucketIndex 索引处,并让新的 Entry 指向原来的 Entry 
    table[bucketIndex] = new Entry<K,V>(hash, key, value, e); 
    // 若是 Map 中的 key-value 对的数量超过了极限
    if (size++ >= threshold) 
        // 把 table 对象的长度扩充到 2 倍。
        resize(2 * table.length); 	 // ②
}

上面方法的代码很简单,但其中包含了一个很是优雅的设计:系统老是将新添加的 Entry 对象放入 table 数组的 bucketIndex 索引处——若是 bucketIndex 索引处已经有了一个 Entry 对象,那新添加的 Entry 对象指向原有的 Entry 对象(产生一个 Entry 链),若是 bucketIndex 索引处没有 Entry 对象,也就是上面程序①号代码的 e 变量是 null,也就是新放入的 Entry 对象指向 null,也就是没有产生 Entry 链。数组

  1. get方法
public V get(Object key) 
 { 
	 // 若是 key 是 null,调用 getForNullKey 取出对应的 value 
	 if (key == null) 
		 return getForNullKey(); 
	 // 根据该 key 的 hashCode 值计算它的 hash 码
	 int hash = hash(key.hashCode()); 
	 // 直接取出 table 数组中指定索引处的值,
	 for (Entry<K,V> e = table[indexFor(hash, table.length)]; 
		 e != null; 
		 // 搜索该 Entry 链的下一个 Entr 
		 e = e.next) 		 // ①
	 { 
		 Object k; 
		 // 若是该 Entry 的 key 与被搜索 key 相同
		 if (e.hash == hash && ((k = e.key) == key 
			 || key.equals(k))) 
			 return e.value; 
	 } 
	 return null; 
 }

从上面代码中能够看出,若是 HashMap 的每一个 bucket 里只有一个 Entry 时,HashMap 能够根据索引、快速地取出该 bucket 里的 Entry;在发生“Hash 冲突”的状况下,单个 bucket 里存储的不是一个 Entry,而是一个 Entry 链,系统只能必须按顺序遍历每一个 Entry,直到找到想搜索的 Entry 为止——若是刚好要搜索的 Entry 位于该 Entry 链的最末端(该 Entry 是最先放入该 bucket 中),那系统必须循环到最后才能找到该元素。安全

  1. 为何HashMap的大小要是2的指数次 ?
static int indexFor(int h, int length)   
{   
    return h & (length-1);   
}

key通过hash后,能够取模来进行放入数组,也不会出现越界的状况,之因此没有使用取模,而是按位与的形式,是由于计算机的二进制运算效率比取模效率高。若是Map的大小不是2的进制,咱们设置为7,7的二进制是:111,(length-1)大小是6,按位与是和6进行,6的二进制是:110,结果以下,有些数组中的位置没有被设置,有些重复了,一是致使空间浪费,同时增长了碰撞的概率。数据结构

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

/>>>是无符号右移的意思,这里的做用是将hashCode和本身的高16位作^运算。因为在计算下标时要h & (length-1),而length通常都小于2^16即小于65536。 h & (length-1)的结果始终是h的低16位与(length-1)进行&运算。因此为了让h的低16位更随机,故选择让其和高16位作^运算。多线程

  1. 扩容机制

初始容量是16,扩容因子是0.75,扩容时大小变为原来2倍。0.75是时间和空间成本上一种折衷:增大负载因子能够减小 Hash 表(就是那个 Entry 数组)所占用的内存空间,但会增长查询数据的时间开销,而查询是最频繁的的操做(HashMap 的 get() 与 put() 方法都要用到查询);减少负载因子会提升数据查询的性能,但会增长 Hash 表所占用的内存空间。并发

触发扩容时,会先申请一个2倍于原Node数组大小的数组,而后依次遍历原数组,并从新计算每一个元素在新数组中对应的下标。这样还会致使同一个数组位置上的链表在新数组对应的链表中变为逆序。 less

并发扩容引发的环(死链): https://www.cnblogs.com/wang-meng/p/7582532.htmldom

三.历HashMap的三种方式:

//1. map.Entry是Map的一个内部接口,entrySet()的返回值是一个Set集合,此集合的类型为Map.Entry——Set<Entry<String, String>>。
Map map = new HashMap();
Iterator iterator = map.entrySet().iterator();
while(iterator.hasNext()) {
	Map.Entry entry = iterator.next();
	Object key = entry.getKey();
}

//2
Map map = new HashMap();
Set  keySet= map.keySet();
Iterator iterator = keySet.iterator;
while(iterator.hasNext()) {
	Object key = iterator.next();
	Object value = map.get(key);
}

//3
Map map = new HashMap();
Collection c = map.values();
Iterator iterator = c.iterator();
while(iterator.hasNext()) {
	Object value = iterator.next();
}

HashMap的实现原理(JDK1.8,和JDK1.7的区别):

  1. 当发生hash冲突时,向链表中以尾插法插入ide

  2. 当节点的链表长度超过8之后,转化为红黑树(同时数组容量必须大于64(至少也是4x8=32)时才转化为红黑树,不然触发扩容。这么作是为了防止在数组容量较小的初期,多个值插入同一位置致使引发没必要要的转化)。

    阈值设置为8的缘由是,理想状态下哈希表的每一个箱子中,元素的数量遵照泊松分布,元素个数超过8的几率极小。 而当链表中的元素个数小于6时,红黑树退化为链表。设置为6而不是7,是为了防止当链表容量为7时,频繁的插入和删除一个元素致使频繁的进行链表和红黑树之间的转化。

  3. 扩容时,再也不像1.7那样从新计算每一个元素的新的下标 h&(length-1),而是进行以下判断:

    扩容时遍历每个旧元素,一样用尾插法插入新数组,能够避免出现逆序和死链的状况

ConcurrentHashMap的实现原理(JDK1.7):

http://www.javashuo.com/article/p-aquxfzkq-gq.html

一.数据结构

static final class HashEntry<K,V> { 
            final K key;                 // 声明 key 为 final 型
            final int hash;              // 声明 hash 值为 final 型 
            volatile V value;           // 声明 value 为 volatile 型
            final HashEntry<K,V> next;  // 声明 next 为 final 型 
 
 
            HashEntry(K key, int hash, HashEntry<K,V> next, V value)  { 
                this.key = key; 
                this.hash = hash; 
                this.next = next; 
                this.value = value; 
            } 
     }

ConcurrentHashMap会建立16个Segment,Segment继承ReentrantLock,每一个Segment包含一个HashEntry/[/]数组

static final class Segment<K,V> extends ReentrantLock implements Serializable {  
     private static final long serialVersionUID = 2249069246763182397L;  
             /** 
              * 在本 segment 范围内,包含的 HashEntry 元素的个数
              * 该变量被声明为 volatile 型,保证每次读取到最新的数据
              */  
             transient volatile int count;  
 
 
             /** 
              *table 被更新的次数
              */  
             transient int modCount;  
 
 
             /** 
              * 当 table 中包含的 HashEntry 元素的个数超过本变量值时,触发 table 的再散列
              */  
             transient int threshold;  
 
 
             /** 
              * table 是由 HashEntry 对象组成的数组
              * 若是散列时发生碰撞,碰撞的 HashEntry 对象就以链表的形式连接成一个链表
              * table 数组的数组成员表明散列映射表的一个桶
              * 每一个 table 守护整个 ConcurrentHashMap 包含桶总数的一部分
              * 若是并发级别为 16,table 则守护 ConcurrentHashMap 包含的桶总数的 1/16 
              */  
             transient volatile HashEntry<K,V>[] table;  
 
 
             /** 
              * 装载因子
              */  
             final float loadFactor;  
    }

二.方法

  1. put() 加锁

先根据hashCode找到Segment,再根据hashCode找到数组下标。

ConcurrentHashMap和HashTable同样不容许key或value为null。经过get(k)获取对应的value时,若是获取到的是null时,你没法判断,它是put(k,v)的时候value为null,仍是这个key历来没有作过映射。HashMap是非并发的,能够经过contains(key)来作这个判断。而支持并发的Map在调用m.contains(key)和m.get(key)时,m可能已经不一样了。

  1. get()
V get(Object key, int hash) { 
        if(count != 0) {       // 首先读 count 变量
            HashEntry<K,V> e = getFirst(hash); 
            while(e != null) { 
                if(e.hash == hash && key.equals(e.key)) { 
                    V v = e.value; 
                    if(v != null)            
                        return v; 
                    // 若是读到 value 域为 null,说明发生了重排序,或者get了一个刚插入的entry,而这个entry还没构造好。加锁后从新读取
                    return readValueUnderLock(e); 
                } 
                e = e.next; 
            } 
        } 
        return null; 
    }
     V readValueUnderLock(HashEntry<K,V> e) {  
         lock();  
         try {  
             return e.value;  
         } finally {  
             unlock();  
         }  
     }

因为next是final类型,因此插入新元素只能采用头插法。若是get()的元素正好是别的线程刚插入的,可能还未初始化完成,返回null,因此须要加锁后再获取一次.

因为next是final类型,因此删除元素只能经过从新构造一个链表的方法:将待删除节点前面的结点复制一遍,尾结点指向待删除节点的下一个结点。 待删除节点后面的结点不须要复制,它们能够重用。

若是咱们get的节点是e3,可能咱们顺着链表刚找到e1,这时另外一个线程就执行了删除e3的操做,而咱们线程还会继续沿着旧的链表找到e3返回。这里没有办法实时保证了。不过这也没什么关系,即便咱们返回e3的时候,它被其余线程删除了,暴漏出去的e3也不会对咱们新的链表形成影响。

  1. size()

count是volitel类型变量,由于在累加count操做过程当中,以前累加过的count发生变化的概率很是小,因此ConcurrentHashMap的作法是先尝试2次经过不锁住Segment的方式来统计各个Segment大小,若是统计的过程当中,容器的count发生了变化,则再采用加锁的方式来统计全部Segment的大小。

那么ConcurrentHashMap是如何判断在统计的时候容器是否发生了变化呢?使用modCount变量,在put , remove和clean方法里操做元素前都会将变量modCount进行加1,那么在统计size先后比较modCount是否发生变化,从而得知容器的大小是否发生变化。

ConcurrentHashMap的实现原理(JDK1.8):

  1. put():

JDK1.8的实现已经摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操做,整个看起来就像是优化过且线程安全的HashMap,虽然在JDK1.8中还能看到Segment的数据结构,可是已经简化了属性,只是为了兼容旧版本。

在put时,若是没有hash冲突,则以CAS方式插入;若是有hash冲突,则用Synchronized加锁。

  1. size():

JDK1.8的ConcurrentHashMap使用baseCount以及CounterCell[]数组统计容量的大小。原本可使用CAS的方式更新一个表示容量的字段,可是为了保证高并发下的效率,做者使用CounterCell[]数组的方式,用多个CAS的方式去更新多个表示容量的字段,最后再将这些字段加和表示map的总容量。

size()方法调用sumCount()计算总容量:

final long sumCount() {
    CounterCell[] as = counterCells; CounterCell a;
    long sum = baseCount;
    if (as != null) {
        for (int i = 0; i < as.length; ++i) {
            if ((a = as[i]) != null)
                sum += a.value;
        }
    }
    return sum;
}

能够看出sumCount()就是将baseCount和CounterCell[]中各元素求和。

在put()、remove()等方法中会调用addCount()去修改map的容量:

private final void addCount(long x, int check) {
    CounterCell[] as; long b, s;
    //第一次执行到这里,CounterCell为空,是否执行if内的语句取决于baseCount是否能cas累加成功
    //若是永远没有并发,则永远只累加baseCount。一旦有并发产生,就会初始化CounterCell,再也不累加baseCount
    if ((as = counterCells) != null ||
        !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
        CounterCell a; long v; int m;
        //无竞争
        boolean uncontended = true;
        //第一次执行到这里,as为空,直接执行if内语句进行CounterCell的初始化
        if (as == null || (m = as.length - 1) < 0 ||
            //probe至关于线程的hash码,这里判断当前线程的CounterCell是否初始化过,不然初始化,是则累加对应的cellValue
            (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
            !(uncontended =
              U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
            //初始化CounterCell
            fullAddCount(x, uncontended);
            return;
        }
        if (check <= 1)
            return;
        s = sumCount();
    }
}

能够看出,在没有并发的状况下,只会累加baseCount的值;当有冲突时,才会调用fullAddCount()初始化CounterCell[]:

private final void fullAddCount(long x, boolean wasUncontended) {
    int h;
    //初始化线程的探针
    if ((h = ThreadLocalRandom.getProbe()) == 0) {
        ThreadLocalRandom.localInit();
        h = ThreadLocalRandom.getProbe();
        wasUncontended = true;
    }
    boolean collide = false;                // True if last slot nonempty
    for (;;) {
        //第一次counterCells为空
        //以后counterCells不为空
        CounterCell[] as; CounterCell a; int n; long v;
        if ((as = counterCells) != null && (n = as.length) > 0) {
            //当前线程对应的CounterCell槽位为空,初始化槽位
            if ((a = as[(n - 1) & h]) == null) {
                //cas
                if (cellsBusy == 0) {            // Try to attach new Cell
                    CounterCell r = new CounterCell(x); // Optimistic create
                    //cas
                    if (cellsBusy == 0 &&
                        U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                        boolean created = false;
                        try {               // Recheck under lock
                            CounterCell[] rs; int m, j;
                            if ((rs = counterCells) != null &&
                                (m = rs.length) > 0 &&
                                rs[j = (m - 1) & h] == null) {
                                rs[j] = r;
                                created = true;
                            }
                        } finally {
                            cellsBusy = 0;
                        }
                        if (created)
                            break;
                        continue;           // Slot is now non-empty
                    }
                }
                collide = false;
            }
            else if (!wasUncontended)       // CAS already known to fail
                wasUncontended = true;      // Continue after rehash
            //槽位不为空,再次cas累加cellValue。成功则退出,失败则继续往下执行
            else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
                break;
            //CounterCell容量是否超过cpu核心数,否,则接更新collide标志,使下次再进行到此处时执行扩容
            else if (counterCells != as || n >= NCPU)
                collide = false;            // At max size or stale
            else if (!collide)
                collide = true;
            else if (cellsBusy == 0 &&
                     U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                try {
                    //扩容为原来的2倍
                    if (counterCells == as) {// Expand table unless stale
                        CounterCell[] rs = new CounterCell[n << 1];
                        for (int i = 0; i < n; ++i)
                            rs[i] = as[i];
                        counterCells = rs;
                    }
                } finally {
                    cellsBusy = 0;
                }
                collide = false;
                continue;                   // Retry with expanded table
            }
            //更新probe
            h = ThreadLocalRandom.advanceProbe(h);
        }
        //CELLSBUSY是一个cas方式的自旋锁,旨在初始化CounterCell[]
        else if (cellsBusy == 0 && counterCells == as &&
                 U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
            boolean init = false;
            try {
                if (counterCells == as) {
                    CounterCell[] rs = new CounterCell[2];
                    rs[h & 1] = new CounterCell(x);
                    counterCells = rs;
                    init = true;
                }
            } finally {
                cellsBusy = 0;
            }
            if (init)
                break;
        }
        //获取cas锁失败
        else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
            break;                          // Fall back on using base
    }
}

参考https://www.sohu.com/a/254192521_355142

其实能够看出JDK1.8版本的ConcurrentHashMap的数据结构已经接近HashMap:

  • JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8锁的粒度就是HashEntry(首节点);
  • JDK1.8版本的数据结构变得更加简单,使得操做也更加清晰流畅,由于已经使用synchronized来进行同步,因此不须要分段锁的概念,也就不须要Segment这种数据结构了;
  • JVM的开发团队历来都没有放弃synchronized,并且基于JVM的synchronized优化空间更大,使用内嵌的关键字比使用API更加天然;
  • 在大量的数据操做下,对于JVM的内存压力,基于API的ReentrantLock会开销更多的内存;

HashMap与HashTable

1.不一样点:

HashMap和Hashtable都实现了map接口。

它们的不一样点有:

  1. HashMap容许键和值时null,而Hashtable不容许键或值为null.
  2. Hashtable是同步的,跟适合多线程。HashMap不是,更适合单线程。
  3. HashMap提供了可供迭代的键的集合,所以,HashMap是快速失败的。另外一方面,Hashtable提供了对键的枚举(Enumeration),是安全失败的。
  4. 扩容的参数不同,HashMap要保证每次扩容后是2的次方倍,Hashtable是扩大一倍。
  5. 散列计算不一样。HashMap由于容量是2的次方倍,因此使用减1与的散列方式而非取余,优化了散列速度。Hashtable是直接取余。

2.为什么HashMap容许键和值时null,而Hashtable不容许键或值为null:

Hashtable:

if (value == null) {
            throw new NullPointerException();
        }
Entry<?,?> tab[] = table;
int hash = key.hashCode();

value为null时直接抛错,而计算key.hashCode()时,null没有hashCode()方法,也抛错。

HashMap:

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

当key为null时,将其hash值置为0.

3.Enumeration

http://blog.csdn.net/zhiweianran/article/details/7672433

Hashtable实现了Enumeration

参考:

http://alex09.iteye.com/blog/539545#comments

http://www.admin10000.com/document/3322.html

http://blog.csdn.net/wisgood/article/details/16342343

关于hashCode引起的hash冲突:

http://blog.csdn.net/fenglibing/article/details/8905007

相关文章
相关标签/搜索