Java多线程基础:容器关系图已经HashMap实现分析

4 篇文章 0 订阅
3 篇文章 0 订阅

容器关系图

Collection          接口的接口   对象的集合 
├ List                   子接口      按进入先后有序保存   可重复 
│├ LinkedList                接口实现类   链表   插入删除   没有同步   线程不安全 
│├ ArrayList                  接口实现类   数组   随机访问   没有同步   线程不安全 
│└ Vector                      接口实现类   数组                  同步        线程安全 
│   └ Stack
└ Set                   子接口   不可重复

├ HashSet

│   └ LinkedHashSet
└ TreeSet

 

 

Map                接口      键值对的集合 
├ Hashtable                  接口实现类         同步           线程安全 
├ HashMap                   接口实现类         没有同步    线程不安全

│├ LinkedHashMap

│└ WeakHashMap

└ TreeMap

二、HashMap实现分析

HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。

数组:存储区间连续,占用内存严重,寻址容易,插入删除困难;

链表:存储区间离散,占用内存比较宽松,寻址困难,插入删除容易;
HashMap综合应用了这两种数据结构,实现了寻址容易,插入删除也容易。

HashMap结构示意图如下:

1.8的HashMap做了很大的变化,故分开分析

JDK1.8之前并发问题

  1. 在hashmap做put操作的时候会调用下面方法:
// 新增Entry。将“key-value”插入指定位置,bucketIndex是位置索引。      
    void addEntry(int hash, K key, V value, int bucketIndex) {      
        // 保存“bucketIndex”位置的值到“e”中      
        Entry<K,V> e = table[bucketIndex];      
        // 设置“bucketIndex”位置的元素为“新Entry”,      
        // 设置“e”为“新Entry的下一个节点”      
        table[bucketIndex] = new Entry<K,V>(hash, key, value, e);      
        // 若HashMap的实际大小 不小于 “阈值”,则调整HashMap的大小      
        if (size++ >= threshold)      
            resize(2 * table.length);      
    }  
 在hashmap做put操作的时候会调用到以上的方法。现在假如A线程和B线程同时对同一个数组位置调用addEntry,两个线程会同时得到现在的头结点,然后A写入新的头结点之后,B也写入新的头结点,那B的写入操作就会覆盖A的写入操作造成A的写入操作丢失
  1. 2.删除键值对会调用以下代码
final Entry<K,V> removeEntryForKey(Object key) {      
        // 获取哈希值。若key为null,则哈希值为0;否则调用hash()进行计算      
        int hash = (key == null) ? 0 : hash(key.hashCode());      
        int i = indexFor(hash, table.length);      
        Entry<K,V> prev = table[i];      
        Entry<K,V> e = prev;      
     
        // 删除链表中“键为key”的元素      
        // 本质是“删除单向链表中的节点”      
        while (e != null) {      
            Entry<K,V> next = e.next;      
            Object k;      
            if (e.hash == hash &&      
                ((k = e.key) == key || (key != null && key.equals(k)))) {      
                modCount++;      
                size--;      
                if (prev == e)      
                    table[i] = next;      
                else     
                    prev.next = next;      
                e.recordRemoval(this);      
                return e;      
            }      
            prev = e;      
            e = next;      
        }      
     
        return e;      
    }  
当多个线程同时操作同一个数组位置的时候,也都会先取得现在状态下该位置存储的头结点,然后各自去进行计算操作,之后再把结果写会到该数组位置去,其实写回的时候可能其他的线程已经就把这个位置给修改过了,就会覆盖其他线程的修改。

3. addEntry中当加入新的键值对后键值对总数量超过门限值的时候会调用一个resize操作,代码如下:

// 重新调整HashMap的大小,newCapacity是调整后的容量      
    void resize(int newCapacity) {      
        Entry[] oldTable = table;      
        int oldCapacity = oldTable.length;     
        //如果就容量已经达到了最大值,则不能再扩容,直接返回    
        if (oldCapacity == MAXIMUM_CAPACITY) {      
            threshold = Integer.MAX_VALUE;      
            return;      
        }      
     
        // 新建一个HashMap,将“旧HashMap”的全部元素添加到“新HashMap”中,      
        // 然后,将“新HashMap”赋值给“旧HashMap”。      
        Entry[] newTable = new Entry[newCapacity];      
        transfer(newTable);      
        table = newTable;      
        threshold = (int)(newCapacity * loadFactor);      
    }  
 这个操作会新生成一个新的容量的数组,然后对原数组的所有键值对重新进行计算和写入新的数组,之后指向新生成的数组。当多个线程同时检测到总数量超过门限值的时候就会同时调用resize操作,各自生成新的数组并rehash后赋给该map底层的数组table,结果最终只有最后一个线程生成的新数组被赋给table变量,其他线程的均会丢失。而且当某些线程已经完成赋值而其他线程刚开始的时候,就会用已经被赋值的table作为原始数组,这样也会有问题。

 

 

JDK1.8并发问题

HashMap中的迭代器源码:

abstract class HashIterator {
    Node<K,V> next;        // next entry to return
    Node<K,V> current;     // current entry
    int expectedModCount;  // for fast-fail
    int index;             // current slot

    HashIterator() {
        expectedModCount = modCount;
        Node<K,V>[] t = table;
        current = next = null;
        index = 0;
        if (t != null && size > 0) { // advance to first entry
            do {} while (index < t.length && (next = t[index++]) == null);
        }
    }

    public final boolean hasNext() {
        return next != null;
    }

    final Node<K,V> nextNode() {
        Node<K,V>[] t;
        Node<K,V> e = next;
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        if (e == null)
            throw new NoSuchElementException();
        if ((next = (current = e).next) == null && (t = table) != null) {
            do {} while (index < t.length && (next = t[index++]) == null);
        }
        return e;
    }

    public final void remove() {
        Node<K,V> p = current;
        if (p == null)
            throw new IllegalStateException();
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        current = null;
        K key = p.key;
        removeNode(hash(key), key, null, false, false);
        expectedModCount = modCount;
    }
}
  1. modCount是hashmap中的成员变量。
  2. 在调用put(),remove(),clear(),ensureCapacity()这些会修改数据结构的方法中都会使modCount++。
  3. 在获取迭代器的时候会把modCount赋值给迭代器的expectedModCount变量。此时modCount与expectedModCount肯定相等。
  4. 在迭代元素的过程中如果hashmap调用自身方法使集合发生变化,那么modCount肯定会变,此时modCount与expectedModCount肯定会不相等。
  5. 在迭代过程中,只要发现modCount!=expectedModCount,则说明结构发生了变化也就没有必要继续迭代元素了。此时会抛出ConcurrentModificationException,终止迭代操作。

HashMap并发问题解决方案

HashMap并发问题解决方案有如下几种:

  1. Synchronized关键字
  2. Lock锁
  3. 同步类容器
  4. 并发类容器

 

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值