HashMap与ConcurrentHashMap

1.HashMap三大重点

1.1基本结构

HashMap存储的是存在映射关系的键值对,通过计算key的hashCode再对其取模,来确定键值对在数组中的位置,假如产生碰撞则使用链表或红黑树,key最好使用不可变类型的对象,否则对象产生变化时重新计算的hashCode值会与之前不一样导致查找错误。
如果采用比较好的哈希算法来运算出的hashCode是比较分散均匀的则查找效率能达到O(1),如果哈希冲突比较强烈,数组将会退化成链表查找效率则会变为O(n),JDK8中做了红黑树的优化,当链表节点大于8个时则会转变为红黑树,红黑树的查找效率为O(logn)。
因而HashMap的关键点就在于尽量避免键值对的哈希碰撞,查找效率就会更高,这主要通过两个方面解决:1.元素分布策略 2.动态扩容
分布策略:1.将数组长度始终保持为2的次幂
2.将哈希值的高位参与运算
3.通过位与操作来等价取模操作

动态扩容:底层数组的长度始终是2的次幂,所以数组长度length的二进制表示会在高位多出1bit,扩容时length会参与位与操作来确定元素所在数组中的新位置,所以原数组中的元素所在位置要么保持不动,要么移动2次幂个位置,这样就能提高动态扩容的效率

1.2 源码分析

1.2.1存储结构

HashMap的存储结构是数组+链表,其中数组存储的元素叫Node,Node是对Entry的实现,Entry是一个接口,Node是其实现类,源码如下:

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

        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);//重写了Object的hashCode方法
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) { //重写了Object的equals方法
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }

1.2.3HashMap成员属性

    transient Node<K,V>[] table; //table为Node类型的数组

   
    transient Set<Map.Entry<K,V>> entrySet; //Map.Entry的泛型为Node的上层接口,存放map中所有的Entry便于遍历


    transient int size; //table中实际被使用的元素数量


    transient int modCount; //计数器,记录HashMap结构发生变化的次数,扩容等


    int threshold; // threshold = size * loadFactor


    final float loadFactor; //负载因子,默认0.75

Java进程之间在进行对象传输时或对对象进行持久化操作时需要将对象序列化二进制流以便于传输或存储,transient修饰的将防止Serializable序列化,比如这些敏感的字段类库开发者不希望被序列化保存到本地,而是序列化后让它们消失。

1.3扩容机制

负载因子0.75

   static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    static final float DEFAULT_LOAD_FACTOR = 0.75f;

从源码中可以看出HashMap的默认初始长度是16,负载因子是0.75,当table中的元素使用了75%(即threshold默认为8)后会扩容,每次按两倍扩容,根据泊松分布可以得到在0.75处的哈希碰撞几率是最小的,因而是使用0.75作为默认负载因子,如果面临特殊的开发情况以及具有优秀的概率知识开发者是可以手动控制负载因子的,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);
    }

但是一般并不建议手动修改。

数组2倍扩容

元素的位置是通过对其key的hashCode进行取模而得出的,取模运算会比较耗性能,如果数组长度总是2的n次方,用hashCode与2^n - 1 进行取模运算就等同于用hashCode & 2^n -1 ,就可以将取模运算转换为更加轻量的位与运算(位运算直接对二进制进行运算),源码中也是这样的:

   static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);//哈希值进行了右移16为再进行异或操作
    }
---
 Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e; //哈希值与长度-1 进行与运算

将key的哈希值右移16为在与原哈希值进行异或运算可以保留高位和低位的信息,这样的值更具有代表性可以减少碰撞。
注:异或不同为1,相同为0

1.4put操作流程

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //先判断数组table是否为空或长度为0
        if ((tab = table) == null || (n = tab.length) == 0)
        //是的话就用resize()方法扩容
            n = (tab = resize()).length;
            //计算要插入元素的下标索引,系统类库所提供的哈希算法是(n-1)&hash
            //源码中的hash计算方式为(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)
        if ((p = tab[i = (n - 1) & hash]) == null)
        //如果此位置是空直接插入到此位置上
            tab[i] = newNode(hash, key, value, null);
            //否则就是数组此位置上已经有元素了,必须链接到相应位置上,此时又分链接到链表和红黑树上两种情况
        else {
            Node<K,V> 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<K,V>)p).putTreeVal(this, tab, hash, key, value);
                //否则就是链接到后面的链表上
            else {
            //遍历链表
                for (int binCount = 0; ; ++binCount) {
                //遍历到末尾也没有找到K相同的节点就尾插
                    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;
                }
            }
     //e不为空替换value值
            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;
    }

在这里插入图片描述
其中大体是如果table为空或length为0则会进行扩容操作,其他情况在插入之后才会扩容

1.5线程安全情况

HashMap不是线程安全的,很多操作都是不带加锁的。

2.ConcurrentHashMap

HashMap存在线程安全问题,那在每次使用时都加上锁不就可以了,因而加锁版的HashMap即Hashtable,Hashtable中的put/get操作都是直接加上synchronized,简单粗暴。但在多线程中锁住整个map来阻塞其他线程效率十分低下。

public synchronized V put(K key, V value) {
        // Make sure the value is not null
        if (value == null) {
            throw new NullPointerException();
        }

        // Makes sure the key is not already in the hashtable.
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        @SuppressWarnings("unchecked")
        Entry<K,V> entry = (Entry<K,V>)tab[index];
        for(; entry != null ; entry = entry.next) {
            if ((entry.hash == hash) && entry.key.equals(key)) {
                V old = entry.value;
                entry.value = value;
                return old;
            }
        }

        addEntry(hash, key, value, index);
        return null;
    }   

 public synchronized V get(Object key) {
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                return (V)e.value;
            }
        }
        return null;
    }

2.1ConcurrentHashMap1.7分段锁

在Hashtable中锁住整个资源会效率底下,但将数组分段,只锁数组的一段数据可以一定程度上减少锁的竞争
在这里插入图片描述

2.2源码分析

2.2.1继承情况

在这里插入图片描述
继承了AbstractMap抽象类,其内部都是一些通用的方法
实现了ConcurrentMap的接口,其中有四个方法,是对map的增删改查,但要求实现类需要保证这些操作的线程安全

2.2.2静态变量

static final int DEFAULT_CAPACITY = 16; //HashEntry数目的初始值

static final float LOAD_FACTOR = 0.75f; //加载因子,和HashMap一样

static final int DEFAULT_CONCURRENCY_LEVEL = 16; //并发等级

static final int MAXIMUM_CAPACITY = 1 << 30; //所有HashEntry数目的最大值

static final int MIN_SEGMENT_TABLE_CAPACITY = 2; //Segment数组最小长度
 
static final int MAX_SEGMENTS = 1 << 16; //Segment数组最大长度

static final int RETRIES_BEFORE_LOCK = 2; //重试次数

2.2.3成员变量

final int segmentMask; //segment的掩码,用来对segment进行定位,判断哪个segment

final int segmentShift; //segment的偏移,segment中的索引

final Segment<K,V>[] segments; //segments数组,类似于整个ConcurrentHashMap的外层数据结构

transient Set<K> keySet;  

transient Set<Map.Entry<K,V>> entrySet;

transient Collection<V> values;  

2.2.4内部类HashEntry与Segment

与HashMap中的Node类似

static final class HashEntry<K,V> {
        final int hash;
        final K key;
        volatile V value;
        volatile HashEntry<K,V> next;

        HashEntry(int hash, K key, V value, HashEntry<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

        /**
         * Sets next field with volatile write semantics.  (See above
         * about use of putOrderedObject.)
         */
        final void setNext(HashEntry<K,V> n) {
            UNSAFE.putOrderedObject(this, nextOffset, n);
        }

        // Unsafe mechanics
        static final sun.misc.Unsafe UNSAFE; //可以理解为一个指针
        static final long nextOffset;//偏移量,可以简单的理解为内存地址
        static {
            try {
                UNSAFE = sun.misc.Unsafe.getUnsafe();//获取这个节点对应的内存指针
                Class k = HashEntry.class;//
                nextOffset = UNSAFE.objectFieldOffset
                    (k.getDeclaredField("next")); //获取当前节点的next节点对于当前节点指针的偏移量
                    //通过UNSAFE中有方法直接能够获取到当前引用变量的初始内存地址
                    //通过初始内存地址和引用变量内部的局部变量的偏移量就可以通过Unsafe直接读取到对应的参数值
            } catch (Exception e) {
                throw new Error(e);
            }
        }
    }
static final class Segment<K,V> extends ReentrantLock implements Serializable { 

    Private static final long seriaVersionUID = 2249069246763182397L;

    private final int MAX_SCAN_RETRIES =  Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1; //指定重试次数,多线程中只有一个线程能put成功进行读写操作,其他线程通过tryLock的方式重试并可以去做其他工作,这里即重试次数
    
    transient volatile int count;  //HashEntry数组元素个数
    
    transient int modCount;  //HashEntry数组修改次数
    
    transient int threshold;  //下一次需要扩容的阈值
    
    transient volatile HashEntry<K,V>[] table; 
    
    final float loadFactor;  //负载因子

    Segment(float if,int threshold,HashEntry<K,V>[] tab){
           this.loadFactor = if;
           this.threshold = threshold;
           this.table = tab;
    }
}

成员方法:
final V put(K key, int hash, V value, boolean onlyIfAbsent )

private void rehash( HashEntry<K,V> node )

private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value )private void scanAndLock(0bject key, int hash) 

final V remove(0bject key, int hash, Object value)

final boolean replace(K key, int hash, V oldValue, V newValue )final V replace(K key, int hash, V value)

final void clear( )

Segment继承自ReentrantLock,其同步控制还是依赖于ReentrantLock,Segment数组中一个Segment对象就是一把锁,一个Segment对象对应了一个HashEntry数组,这个HashEntry数组中的数据同步依赖于同一把锁,不同HashEntry数组读写互不干扰即形成所谓的分段锁

2.2.5put操作流程

构造方法:

                           //  数组初始长度16        加载因子0.75        并发等级默认16
public ConcurrentHashMap (int initialCapacity, float loadFactor, int concurrencyLevel) {
     if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0) throw new IllegalArgumentException(); //校验参数
     if(concurrencyLevel > MAX_SEGMENTS)
        concurrencyLevel = MAX_SEGMENTS;
     // Find power-of-two sizes best matching ar guments
     int sshift = 0; //
     int ssize = 1; //Sengment数组初始长度1
     while (ssize < concurrencyLevel) {
        ++sshift ; //sshift自增1
        ssize <<= 1; //数组扩容2倍
     }
     this.segmentShift = 32 - sshift;
     this.segmentMask = ssize - 1; //数组长度减1,用来代替取模做位与运算用的

     if (initialCapacity > MAXIMUM CAPACITY)
         initialCapacity = MAXIMUM CAPACITY;
     int c = initialCapacity / ssize;
     if (c * ssize < initialCapacity)
          ++c;

     int cap = MIN_SEGMENT_TABLE_CAPACITY; //计算后的HashEntry数组的长度
     while(cap < c)
           cap <<= 1;

     // create segments and segments[0]
     Segment<K,V> s0 = new Segment<K,V>(loadFactor, (int)(cap * loadFactor),(HashEntry<K,V>[])new HashEntry[cap]);
     Segment<K,V>[] ss = (Segment<K ,v>[])new Segment[ssize];
     UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
     this.segments = ss ; //向Segment数组中加入第一个元素

put方法有两个,除了最后一行都一样,可能是为了屏蔽调用方的难度:

    public V put(K key, V value) {
        Segment<K,V> s; 
        if (value == null)
            throw new NullPointerException();
        int hash = hash(key); //首先对key进行哈希
        int j = (hash >>> segmentShift) & segmentMask; //取模j与HashMap中一样
        if ((s = (Segment<K,V>)UNSAFE.getObject           
             (segments, (j << SSHIFT) + SBASE)) == null)  
            s = ensureSegment(j); //使用ensureSegment方法取出对象,是UnSafe操作
        return s.put(key, hash, value, false); //然后调用Segment对象的put方法
    }


    public V putIfAbsent(K key, V value){
        Segment<K,V> s;
        if (value == null)
            throw new NullPointerException();
        int hash = hash(key);
        int j = (hash >>> segmentShift) & segmentMask; 
        if ((s = (Segment<K ,V> )UNSAFE.getObject
             (segments, (j << SSHIFT) + SBASE)) == null)
             s = ensureSegment(j);
        return s.put(key, hash, value, true);

Segment的put:

final V put(K key, int hash, V value, boolean onlyIfAbsent) {
        HashEntry<K,V> node = tryLock() ? null :
            scanAndLockForPut(key, hash, value); //三目运算符,如果获得了锁Node为null,否则执行scanAndLockForPut方法
        V oldValue;
        try {
            HashEntry<K,V>[] tab = table;
            int index = (tab.length - 1) & hash; //计算index
            HashEntry<K,V> first = entryAt(tab, index); // 通过index拿到HashEntry链表的头节点
            for (HashEntry<K,V> e = first;;) { //通过头节点去遍历链表
                if (e != null) { //在头节点不为null的情况下
                    K k;
                    if ((k = e.key) == key ||  //如果key已经存在覆盖value并退出
                        (e.hash == hash && key.equals(k))) {
                        oldValue = e.value;
                        if (!onlyIfAbsent) {
                            e.value = value;
                            ++modCount;
                        }
                        break;
                    }
                    e = e.next; 
                }
                else { //没找到说明e为null
                    if (node != null) //判断node是否被初始化,如果当前线程是通过tryLock直接获得的锁则node为null,需要对node进行构造
                        node.setNext(first); //将node设置为当前节点的next
                    else
                        node = new HashEntry<K,V>(hash, key, value, first); //将node设置为头节点
                    int c = count + 1;
                    if (c > threshold && tab.length < MAXIMUM_CAPACITY) //判断HashEntry数组需要扩容
                        rehash(node); //如果超过阈值,则进行rehash操作
                    else
                        setEntryAt(tab, index, node); //将node放入HashEntry数组的指定位置
                    ++modCount;
                    count = c;
                    oldValue = null;
                    break; 
                }
            }
        } finally {
            unlock(); //结束操作释放锁
        }
        return oldValue;
    }

如果线程tryLock没有拿到锁就会进入scanAndLockForPut方法,返回类型是node,node即HashEntry链表中不存在当前key值时需要去构造存储当前key和value的节点,也就是线程最终还是等待到了锁,线程预先去创建了node在等到锁后直接返回了node,但问题的关键点在于当前线程并不知道HashEntry链表中是否已经存在了当前key,因而这个方法主要用到了预创建的思想

    private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
        //根据hash值找到segment中的HashEntry节点
        HashEntry<K,V> first = entryForHash(this, hash); //首先获取头结点
        HashEntry<K,V> e = first;
        HashEntry<K,V> node = null;
        int retries = -1; // negative while locating node
        while (!tryLock()) {  //持续遍历该哈希链
            HashEntry<K,V> f; // to recheck first below
            if (retries < 0) {
                if (e == null) {
                    if (node == null) //若不存在要插入的节点,则创建一个新的节点
                        node = new HashEntry<K,V>(hash, key, value, null);
                    retries = 0;
                }
                else if (key.equals(e.key))
                    retries = 0;
                else
                    e = e.next;
            }
            else if (++retries > MAX_SCAN_RETRIES) { 
            //尝试次数超出限制,则进行自旋等待
                lock();
                break;
            }
            else if ((retries & 1) == 0 &&  // 如果发现head改变,说明链表被其他线程改变需要重新尝试创建node
                     (f = entryForHash(this, hash)) != first) {
                e = first = f; // re-traverse if entry changed
                retries = -1;
            }
        }
        return node;
    }

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值