Java容器解析系列(11) HashMap 详解

本篇我们来介绍一个最常用的Map结构——HashMap

关于HashMap,关于其基本原理,网上对其进行讲解的博客非常多,且很多都写的比较好,所以....

这里直接贴上地址:

关于hash算法:

Hash算法

Hash时取模一定要模质数吗?

关于HashMap:

深入Java集合学习系列:HashMap的实现原理

漫画:什么是HashMap?

JDK 源码中 HashMap 的 hash 方法原理是什么?

What is the use of Holder class in HashMap?(HashMap.Holder)

JDK7与JDK8中HashMap的实现(jdk8对HashMap的红黑树改进)

读完上面的内容,应该对HashMap有了很深的理解,这里补充1点助于深入的理解:

高并发情况下,HashMap为什么会出现死循环

漫画:高并发下的HashMap

疫苗:JAVA HASHMAP的死循环

上述博客均对多线程情况下的HashMap出现死循环问题的原理进行了解释,但是没有提供demo进行参考,我在网上也试图找相应的demo,但是没有能够100%复现的;这里我们直接参考疫苗:JAVA HASHMAP的死循环中的数据和过程,修改HashMap源码,提供循环demo,并阐述死锁发生的原因,及如何解决死循环:

首先,修改HashMap源码如下:

import java.util.Map;

public class HashMap<K,V>{
    
    private transient Entry[] table;
    private transient int size;
    private int threshold;
    private final float loadFactor;
    transient int modCount;
    
    public HashMap(int initialCapacity,float loadFactor){
        int capacity = 1;
        while(capacity < initialCapacity)
            capacity <<= 1;
        this.loadFactor = loadFactor;
        threshold = (int)(capacity * loadFactor);
        table = new Entry[capacity];
    }
    
    // 所有的hash值都是1
    final int hash(Object k){
        return 1;
    }
    
    // 让所有的值的索引都是1
    static int indexFor(int h,int length){
        return 1;
    }
    
    public V get(Object key){
        Entry<K,V> entry = getEntry(key);
        return null == entry ? null : entry.getValue();
    }
    
    final Entry<K,V> getEntry(Object key){
        int hash = (key == null) ? 0 : hash(key);
        for(Entry<K,V> e = table[indexFor(hash,table.length)];
            e != null;
            e = e.next){
            System.out.println("e.getKey():" + e.getKey());
            Object k;
            if(e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))){
                return e;
            }
        }
        return null;
    }
    
    public V put(K key,V value,boolean delay){
        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;
                return oldValue;
            }
        }
        modCount++;
        addEntry(hash,key,value,i,delay);
        return null;
    }
    
    void addEntry(int hash,K key,V value,int bucketIndex,boolean delay){
        if((size >= threshold) && (null != table[bucketIndex])){
            resize(2 * table.length,delay);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash,table.length);
        }
        createEntry(hash,key,value,bucketIndex);
    }
    
    void createEntry(int hash,K key,V value,int bucketIndex){
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash,key,value,e);
        size++;
    }
    
    // 扩容,添加了delay参数,用于在测试过程中进行测试
    void resize(int newCapacity,boolean delay){
        Entry[] newTable = new Entry[newCapacity];
        transfer(newTable,delay);
        threshold = (int)(newCapacity * loadFactor);
    }
    
    // 原有的HashMap的transfer函数,会导致死循环,且有对象被丢弃
    // 添加延时选项,用于手动控制多个线程的相对运行过程
    void transfer(Entry<K,V>[] newTable,boolean delay){
        System.out.println("transfer in\t" + Thread.currentThread().toString());
        int newCapacity = newTable.length;
        for(Entry e : table){
            while(null != e){
                Entry<K,V> next = e.next;

                if(delay){
                    try{
                        Thread.sleep(20);
                    }catch(InterruptedException e1){
                        e1.printStackTrace();
                    }
                }
                int i = indexFor(e.hash,newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
        System.out.println("transfer out\t" + Thread.currentThread().toString());
    }
    
    // 解决死循环
    // 方式: 扩容后的数组,在对象rehash的过程中,如果某个位置出现冲突,采用尾插法将Entry插入链表
    // 原理: 出现死循环的原因:在rehash过程中,对原数组某个位置的链表,采用从头开始的遍历方式,在新数组中,如果出现冲突,采用头插法将Entry插入链表
    // 注意: 这里虽然解决了死循环的问题,但是因为并发修改对象内容,导致遍历过程中某些对象被丢弃的问题还是存在,
    //       所以还是老老实实地用ConcurrentHashMap或者Collections.synchronizedMap()吧
    //void transfer(Entry<K,V>[] newTable,boolean delay){
    //  System.out.println("transfer in\t" + Thread.currentThread().toString());
    //  int newCapacity = newTable.length;
    //  for(Entry e : table){
    //      while(null != e){
    //          Entry<K,V> next = e.next;
    //          if(delay){
    //              try{
    //                  Thread.sleep(20);
    //              }catch(InterruptedException e1){
    //                  e1.printStackTrace();
    //              }
    //          }
    //          int i = indexFor(e.hash,newCapacity);
    //          Entry tmp = newTable[i];
    //          if(tmp == null){
    //              newTable[i] = e;
    //              newTable[i].next = null;
    //          }else{
    //              // 尾插法
    //              while(tmp.next != null){
    //                  System.out.println(tmp.next.getKey());
    //                  System.out.println("----------------------");
    //                  tmp = tmp.next;
    //              }
    //              tmp.next = e;
    //              tmp.next = null;
    //          }
    //          e = next;
    //      }
    //  }
    //  System.out.println("transfer out\t" + Thread.currentThread().toString());
    //}
    
    static class Entry<K,V> implements Map.Entry<K,V>{
        final K key;
        V value;
        Entry<K,V> next;
        int hash;
        Entry(int h,K k,V v,Entry<K,V> n){
            value = v;
            next = n;
            key = k;
            hash = h;
        }
        public final K getKey(){
            return key;
        }
        public final V getValue(){
            return value;
        }
        public final V setValue(V newValue){
            V oldValue = value;
            value = newValue;
            return oldValue;
        }
        // hashCode永远为1
        public final int hashCode(){
            return 1;
        }
        public final String toString(){
            return getKey() + "=" + getValue();
        }
    }
}
public class HashMapInfinitLoop{
    
    public static void main(String[] args) throws InterruptedException{
        HashMap<Integer,Integer> map = new HashMap<>(4,0.8f);
        map.put(5,55,false);
        map.put(7,77,false);
        map.put(3,33,false);
        
        new Thread("Thread1"){
            public void run(){
                map.put(17,77,true);
                System.out.println("put 17 finished");
            }
        }.start();

        new Thread("Thread2"){
            public void run(){
                map.put(23,33,false);
                System.out.println("put 23 finished");
            }
        }.start();

        Thread.sleep(2000);
        // 此处get()死循环
        System.out.println(map.get(5));
        
    }
    
}

运行上述demo,控制台输出如下:

transfer in Thread[Thread1,5,main]
transfer in Thread[Thread2,5,main]
transfer out    Thread[Thread2,5,main]
put 23 finished
transfer out    Thread[Thread1,5,main]
put 17 finished
e.getKey():17
e.getKey():23
e.getKey():3
e.getKey():7
e.getKey():3
e.getKey():7
.......

注释掉原有的transfer(),使用解决死循环的transfer(),运行结果如下:

transfer in Thread[Thread1,5,main]
transfer in Thread[Thread2,5,main]
transfer out    Thread[Thread2,5,main]
put 23 finished
transfer out    Thread[Thread1,5,main]
put 17 finished
e.getKey():17
e.getKey():23
e.getKey():3
null

发现死循环问题是没有了,但是还是存在数据被丢弃的情况.

so,it sucks

猜你喜欢

转载自www.cnblogs.com/jamesvoid/p/9809452.html