HashTable源码分析

本文主要介绍HashTable以及其与HashMap的异同点等方面,HashTable源码分析主要从继承结构,基本属性,构造方法,核心方法四个方面介绍,以下为正文:

  • HashTable简介

 HashTable的底层同样基于数组+链表实现的,存储的元素也是key-value键值对,但HashTable是JDK1.0引入的类,是线程安全的,可以用于多线程环境中,具体内容详见源码分析。

  • 继承结构

public class Hashtable<K,V> extends Dictionary<K,V>
        implements Map<K,V>, Cloneable, java.io.Serializable { }

         由源码可以看出:HashTable继承于Dictionary,这是一个比较早的实现类(since JDK1.1),含有一些特殊方法,keys()  /elements()     枚举类型遍历  ;实现了Map接口,Cloneable接口,Serializable接口(支持实现序列化);

 

  • 基本属性

//数组table,内有静态内部类Entry
private transient Entry<K,V>[] table;
//hashTable中的数据个数
private transient int count;
//阈值
private int threshold;
//加载因子(默认0.75f)
private float loadFactor;
//版本控制(修改次数),用于实现fast-fail机制
private transient int modCount = 0;
//版本序列号
private static final long serialVersionUID = 1421746759512286392L;

static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;

 

  • 构造方法(4个)

一、//指定容量和加载因子的构造函数
    public Hashtable(int initialCapacity, float loadFactor) {
//参数合法性校验
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                    initialCapacity);
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal Load: "+loadFactor);

        if (initialCapacity==0)
            initialCapacity = 1;
        this.loadFactor = loadFactor;
        table = new Entry[initialCapacity];//创建数组
        threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);//阈值初始化
        initHashSeedAsNeeded(initialCapacity);
    }

  二、//指定容量的构造函数
    public Hashtable(int initialCapacity) {
        this(initialCapacity, 0.75f);//加载因子默认0.75
    }

    三、//默认构造函数
    public Hashtable() {
        this(11, 0.75f);//初始容量默认11,加载因子默认0.75
    }
    四、//包含“子Map”的构造函数
    public Hashtable(Map<? extends K, ? extends V> t) {//集合
        this(Math.max(2*t.size(), 11), 0.75f);
        putAll(t);
    }
  • 核心方法

1、添加元素put()方法  

    public synchronized V put(K key, V value) {//方法修饰符为synchronized,表明线程安全
        // Make sure the value is not null  确保value不为null,否则抛出空指针异常
        if (value == null) {
            throw new NullPointerException();
        }

        // Makes sure the key is not already in the hashtable.//确保key不在hashTable中
        Entry tab[] = table;
        int hash = hash(key);//hash过程(hashSeed ^ k.hashCode();key若为null,那么null.hashcode会抛出异常,表明key不能为null)
        int index = (hash & 0x7FFFFFFF) % tab.length;//确定其在table[]中的具体位置,先对hash取正,再对数组长度取余;
        for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
//key重复覆盖返回,只用equals判断key是否相等,原因,hashTable中key不为null;HashMap中用"=="||"equals"判断原因是,其key可为null;
                V old = e.value;
                e.value = value;
                return old;
            }
        }

        modCount++;
        if (count >= threshold) {//元素超过阈值,调用rehash()方法
            // Rehash the table if the threshold is exceeded
            rehash();

            tab = table;
            hash = hash(key);
            index = (hash & 0x7FFFFFFF) % tab.length;
        }

        // Creates the new entry.创建新节点,插入index位置(头插),将e设为下一个元素
        Entry<K,V> e = tab[index];
        tab[index] = new Entry<>(hash, key, value, e);
        count++;
        return null;
    }

//hash()
private int hash(Object k) {
    // hashSeed will be zero if alternative hashing is disabled.
    return hashSeed ^ k.hashCode();
}
//rehash()
    protected void rehash() {
        int oldCapacity = table.length;
        Entry<K,V>[] oldMap = table;

        // overflow-conscious code
        int newCapacity = (oldCapacity << 1) + 1;//扩容  2*原数组长度+1
        if (newCapacity - MAX_ARRAY_SIZE > 0) {
            if (oldCapacity == MAX_ARRAY_SIZE)
                // Keep running with MAX_ARRAY_SIZE buckets
                return;
            newCapacity = MAX_ARRAY_SIZE;
        }
        Entry<K,V>[] newMap = new Entry[newCapacity];

        modCount++;
        threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
        boolean rehash = initHashSeedAsNeeded(newCapacity);

        table = newMap;

        for (int i = oldCapacity ; i-- > 0 ;) {
            for (Entry<K,V> old = oldMap[i] ; old != null ; ) {
                Entry<K,V> e = old;
                old = old.next;

                if (rehash) {
                    e.hash = hash(e.key);
                }
                int index = (e.hash & 0x7FFFFFFF) % newCapacity;
                e.next = newMap[index];
                newMap[index] = e;
            }
        }

在put过程,可以看出其数据结构与HashMap相同,是一个Entry数组;有一点明显区别的是:HashTable再hash过程后计算索引时, int index = (hash & 0x7FFFFFFF) % tab.length,其中hash & 0x7FFFFFFF是为了避免负值出现,对数组长度取余是为了限定在数组范围内,而HashMap中计算索引: h & (length-1)是对(hash & 0x7FFFFFFF) % tab.length的优化,因为位运算比取余运算的效率高。

还有一点,HashMap与HashTable的最大值是不同的,HashMap的最大值是1<<30;为int范围内2的次幂最大值,而HashTable的最大值,是指虚拟机实现对array的长度有限制。

2、删除remove()方法

public synchronized V remove(Object key) {
    Entry tab[] = table;
    int hash = hash(key);
    int index = (hash & 0x7FFFFFFF) % tab.length;
    for (Entry<K,V> e = tab[index], prev = null ; e != null ; prev = e, e = e.next) {
        if ((e.hash == hash) && e.key.equals(key)) {
            modCount++;
            if (prev != null) {
                prev.next = e.next;
            } else {
                tab[index] = e.next;
            }
            count--;
            V oldValue = e.value;
            e.value = null;
            return oldValue;
        }
    }
    return null;
}

3、获取元素get()

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

//首先通过 hash()方法求得 key 的哈希值,然后根据 hash 值得到 index 索引(上述两步所用的算法与 put 方法都相同)。然后迭代链表,返回匹配的 key 的对应的 value;找不到则返回 null。
  • HashMap与HashTable的异同点

区别:

继承结构

HashTable 基于 Dictionary 类,而 HashMap 是基于 AbstractMap。Dictionary 是任何可将键映射到相应值的类的抽象父类,而 AbstractMap 是基于 Map 接口的实现,它以最大限度地减少实现此接口所需的工作。

key和value的null值问题

HashMap 的 key 和 value 都允许为 null,而 Hashtable 的 key 和 value 都不允许为 null。HashMap 遇到 key 为 null 的时候,调用 putForNullKey 方法进行处理,而对 value 没有处理;Hashtable遇到 null,直接返回 NullPointerException。

线程安全

Hashtable 方法是同步,而HashMap则不是。Hashtable 中的几乎所有的 public 的方法都是 synchronized 的,而有些方法也是在内部通过 synchronized 代码块来实现。

hash函数不同

HahTable直接使用key的hashcode值,而hashMap对key的hash值重新计算,详见源码;

扩容方式不同,初始容量不同

扩容:

hashMap    resize(2 * table.length)   2倍扩容

hashTale  int newCapacity = (oldCapacity << 1) + 1       2倍+1扩容

初始数组容量:

HashMap    16;求数组容量为2的次幂;

HashTable   11;不要求数组容量为2的次幂;

最大值限制不同

HashMap   最大容量为  1<<30(即2^30)

HashTable   最大容量指虚拟机实现对array的长度有限制,也就是不超过内存即可;

 

特有方法

HashTable:特有方法contains();

速度效率问题:

单线程hashMap快于hashTable

原因:synchronized涉及用户态和内核态切换,用时较长;多线程情况下,相同条件下,hashMap效率高,但还是使用hashTable(线程安全)

相同: 

1. 它们都是存储键值对(key - value)的散列表,而且都是采用链地址法 实现的。

2. 添加键值对:通过 key 计算出哈希值,再计算出数组的索引,根据索引去遍历单链表,如果链表中已经存在这个key 值,则覆盖原来的value,否则,加入新的节点。

3. 删除键值对: 通过 key 计算出哈希值,再计算出数组的索引,根据索引去遍历单链表,从单链表中删除这个键值对。

  • 遍历方式

1、 迭代器遍历

Iterator遍历三种(键值对,键,值遍历)
           //第一种hashtable遍历方式
           System.out.println("第一种遍历方式");
           for(Iterator<String> iterator=hashtable.keySet().iterator();iterator.hasNext();){
        String key=iterator.next();
        System.out.println("key-----"+key);
        System.out.println("value--------"+hashtable.get(key));
        }

//第二种hashtable遍历方式
        System.out.println("第二种遍历方式");
        for(Iterator<Entry<String, String>> iterator=hashtable.entrySet().iterator();iterator.hasNext();){
        Entry<String,String> entry=iterator.next();
        System.out.println("key---------"+entry.getKey());
        System.out.println("value------------"+entry.getValue());
        }

//第三种hashtable遍历方式
        System.out.println("第三种遍历方式");
        for(Map.Entry<String, String> entry: hashtable.entrySet()){
        System.out.println("key---------"+entry.getKey());
        System.out.println("value--------"+entry.getValue());
        }

2、枚举遍历

//枚举遍历(2种)

 System.out.println("第一种遍历方式");
        Enumeration<String> e = hashtable.keys();
        while(e.hasMoreElements()){
            String key = e.nextElement();
            System.out.println("key"+"  "+key);
            System.out.println("value"+"  "+hashtable.get(key));
        }

//获取所有值
        System.out.println("第二种遍历方式");
        Enumeration<String> e1 = hashtable.elements();
        while(e1.hasMoreElements()){
            String s = (String) e1.nextElement();
            System.out.println(s);
        }
    }
  • 线程安全问题

 HashTable是线程安全类,而HashMap是非线程安全类,那么HashMap非线程安全的原因是什么呢?如何解决呢?

     HashMap会在并发执行put操作时引起死循环,CPU利用率接近100%,原因是多线程会导致链表形成环链,导致死循环,实际上环链问题是在出现在transfer()方法扩容操作时出现的;在自动调整大小机制期间,如果线程尝试放入或获取对象,则映射可能使用旧索引值,并且不会找到条目所在的新存储桶。最糟糕的情况是2个线程同时放入数据,2个put()调用同时调整Map的大小。由于两个线程同时修改了链接列表,因此Map最终可能会在其链接列表中出现内部循环。如果尝试使用内部循环获取列表中的数据,则get()将永远不会结束。详细问题可以参考以下博客:

https://coolshell.cn/articles/9606.html

http://firezhfox.iteye.com/blog/2241043

如何在多线程下使用HashMap?(3种方式)

1、HashTable

由于HashTable是线程安全类,put与get等方法都用synchronized关键字修饰,保证线程安全;当一个线程访问HashTable的同步方法时,其他线程也要访问同步方法,会被阻塞;比如,一个线程要访问put方法,其他线程不但不可以访问put方法,连get方法也不可以访问,所以效率很低,很少使用;

2、ConcurrentHashMap(CHM)

CHM引入了分割,并提供了HashTable支持的所有的功能。在CHM中,支持多线程对Map做读操作,并且不需要任何的blocking。这得益于CHM将Map分割成了不同的部分,在执行更新操作时只锁住一部分。根据默认的并发级别(concurrency level),Map被分割成16个部分,并且由不同的锁控制。这意味着,同时最多可以有16个写线程操作Map。试想一下,由只能一个线程进入变成同时可由16个写线程同时进入(读线程几乎不受限制),性能的提升是显而易见的。但由于一些更新操作,如put(),remove(),putAll(),clear()只锁住操作的部分,所以在检索操作不能保证返回的是最新的结果。

另一个重要点是在迭代遍历CHM时,keySet返回的iterator是弱一致和fail-safe的,可能不会返回某些最近的改变,并且在遍历过程中,如果已经遍历的数组上的内容变化了,不会抛出ConcurrentModificationExceptoin的异常。

CHM默认的并发级别是16,但可以在创建CHM时通过构造函数改变。毫无疑问,并发级别代表着并发执行更新操作的数目,所以如果只有很少的线程会更新Map,那么建议设置一个低的并发级别。另外,CHM还使用了ReentrantLock来对segments加锁。

此处内容参考自:https://yemengying.com/2015/11/06/%E3%80%90%E8%AF%91%E3%80%91%E5%A6%82%E4%BD%95%E5%9C%A8java%E4%B8%AD%E4%BD%BF%E7%94%A8ConcurrentHashMap/

3、SynchronizedMap

源码(此处基于JDK1.8)

public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {
    return new SynchronizedMap<>(m);
}
private static class SynchronizedMap<K,V>
    implements Map<K,V>, Serializable {
    private static final long serialVersionUID = 1978198479659022715L;

    private final Map<K,V> m;     // Backing Map
    final Object      mutex;        // Object on which to synchronize

    SynchronizedMap(Map<K,V> m) {
        this.m = Objects.requireNonNull(m);
        mutex = this;
    }

    SynchronizedMap(Map<K,V> m, Object mutex) {
        this.m = m;
        this.mutex = mutex;
    }

    public int size() {
        synchronized (mutex) {return m.size();}
    }
    public boolean isEmpty() {
        synchronized (mutex) {return m.isEmpty();}
    }
    public boolean containsKey(Object key) {
        synchronized (mutex) {return m.containsKey(key);}
    }
    public boolean containsValue(Object value) {
        synchronized (mutex) {return m.containsValue(value);}
    }
    public V get(Object key) {
        synchronized (mutex) {return m.get(key);}
    }

    public V put(K key, V value) {
        synchronized (mutex) {return m.put(key, value);}
    }
    public V remove(Object key) {
        synchronized (mutex) {return m.remove(key);}
    }
    public void putAll(Map<? extends K, ? extends V> map) {
        synchronized (mutex) {m.putAll(map);}
    }
    public void clear() {
        synchronized (mutex) {m.clear();}
    }

    //其余方法省略

可以看出:调用该方法会返回一个SynchronizedMap类的对象,而SynchronizedMap类中使用了Synchronized同步关键字保证对Map的操作是线程安全的。

使用该方法:

Map<String,String> hashmap = Collections.synchronizedMap(new HashMap<>());

关于三种方法的效率问题,大家可以进行代码测试,结果:CHM的效率远高于其他两种方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值