HashMap和Hashtable源码分析

我们都知道HashMap和Hashtable的区别是:
HashMap线程不安全,允许NULL值和Null键。
Hashtable线程安全,不允许Null值和Null键。
下面我们用多线程去分别操作HashMap和Hashtable看看会出现什么状况。
首先是用多线程去操作HashMap:

package com.xql.thread.MyThread;

import java.util.HashMap;
import java.util.Map;

public class HashMapManyThread {

    static Map<String,String > map =new HashMap<String, String>(16);//初始化容量

    public static  class TestHashMapThread implements Runnable{

        int start=0;

        public TestHashMapThread(int start){

            this.start=start;
        }

        @Override
        public void run() {

            for (int i = 0; i <100000 ; i+=2) {

                System.out.println("--puting----");

                map.put(Integer.toString(i),String.valueOf(Math.random()*100));
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {

        Thread[] threads =new Thread[100];

        for (int i = 0; i <threads.length ; i++) {

            threads[i]=new Thread(new TestHashMapThread(i));

        }


        for (int i = 0; i <100 ; i++) {

            threads[i].start();
        }

        System.out.println(map.size());
    }
}

《HashMap和Hashtable源码分析》
直接造成了cpu100%的占用,我是刚不住了只好停下来了,怕爆炸!!然后我们利用jstack去查看线程状态
《HashMap和Hashtable源码分析》
我们可以看到线程已经进入了运行中而且还是循环中,后面进入了死锁状态。这就体现了hashmap的线程不安全。
接着我们来用多线程操作hashtable:

package com.xql.thread.demo2;
 
import java.util.Hashtable;
import java.util.Iterator;
import java.util.concurrent.ConcurrentHashMap;
 
public class MapTest {
 
//	public static ConcurrentHashMap cmap = new ConcurrentHashMap();
	public static Hashtable cmap = new Hashtable();
	public static void main(String[] args) {
		// TODO Auto-generated method stub
 
		MapThreadAdd mt2 = new MapThreadAdd();
		Thread addThread = new Thread(mt2,"");
		addThread.start();
		try{
			Thread.sleep(1);
		}catch(Exception e){
			
		}
		while(true){
			if(cmap.size()>=5){
				Iterator<String> it = cmap.keySet().iterator();
				while(it.hasNext()){
					System.out.println("************************");
					String key = it.next();
//					it.remove();//通过Iterator修改Hashtable
					String value = (String)cmap.get(key);
					System.out.println("循环key:"+key);
				}
			}
		}
	}
 
}
    package com.xql.thread.demo2;
 
public class MapThreadAdd implements Runnable{
	
	public void run(){
		for(int i=0;i<1000;i++){
			MapTest.cmap.put("key"+i, i+"");
			try{
				Thread.sleep(10);
			}catch(Exception e){
				
			}
		}
	}
}

这是在hashtable一边插入一边遍历时就会出错, 原因:Iterator做遍历的时候,HashMap被修改(bb.remove(ele), size-1),Iterator(Object ele=it.next())会检查HashMap的size,size发生变化,抛出错误ConcurrentModificationException。《HashMap和Hashtable源码分析》
解决办法:

  1. 通过Iterator修改Hashtable
    while(it.hasNext()) {
    Object ele = it.next();
    it.remove();
    }

  2. 根据实际程序,您自己手动给Iterator遍历的那段程序加锁,给修改HashMap的那段程序加锁。

  3. 使用“ConcurrentHashMap”替换HashMap,ConcurrentHashMap会自己检查修改操作,对其加锁,也可针对插入操作。
    import java.util.concurrent.*;
    上面我们说了hashmap和hashtable的线程问题,下面我们说说hashmap和hashtable的工作原理:
    hashmap的底层是数组+链表在jdk1.8后底层是数组+链表+二叉树。它的工作原理是:HashMap是基于hashing的原理,我们使用put(key, value)存储对象到HashMap中,使用get(key)从HashMap中获取对象。当我们给put()方法传递键和值时,我们先对键调用hashCode()方法,返回的hashCode用于找到bucket位置来储存Entry对象。”这里关键点在于指出,HashMap是在bucket中储存键对象和值对象,作为Map.Entry。

    HashMap的put方法对key的hashcode做hash,然后通过putVal来进行存储值

    public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
    }
    1、首先判断键值对数组是否null长度是否为0,如果是数组为null长度为0,就对数组进行扩容。
    2、通过key的hash计算数组的索引,这就是key的位置
    3、如果该节点没有数据就返回为null,如果数据是一个二叉树,那就执行增加操作,如果是链表分两种情况一是该链表没有这个节点,另一个是该链表上有这个节点,注意这里判断的依据是key.hash是否一样:
    如果该链表没有这个节点,那么采用尾插法新增节点保存新数据,返回null;如果该链表已经有这个节点了,那么找到该节点并更新新数据,返回老数据。

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
    boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
    n = (tab = resize()).length;
    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) {
    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;
    }
    }
    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;
    }

Hashmap的get方法:通过key的hashcode找到对应节点,通过getNode实现

public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}

1、判断数组不为空,长度大于0且数据已经初始化,根据hash寻找数组中的项不为null
2、bucket中第一项相等,且不止一个节点
3、为二叉树节点时在二叉树中寻找
4、为链表时在链表中寻找

final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }
下面是hashtable的工作原理:

hashtable之所以是线程安全的,原因为在put和get方法上使用synchronized关键字进行修饰。
通过key的hashcode值的寻址,找到在table数组中的位置index.
遍历table[index]链表,找到key值相同的节点的value返回,注意同样在该过程中使用到了key的equal方法,所以key被应用与hashtable时不仅要实现hashcode方法还有实现equal方法。

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

1、有synchronized 关键字保证其线程安全
2、定义了value不能为null,否则会报空指针异常
3、通过key的hashcode找到对应值在数组中的位置
4、遍历table[index]所连接的链表,查找是否已经存在key与需要插入的key值相同的节点,如果存在则直接更新value,并返回旧的value。
5、如果table[index]所连接的链表上不存在相同的key,则通过addEntry()方法将新节点加载链表的开头。
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;

}
hashmap继承了AbstractMap实现了map。Cloneable,Serializable 接口

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable 

hashtable继承了Dictionary实现了map,Cloneable,Serializable 接口

public class Hashtable<K,V>
    extends Dictionary<K,V>
    implements Map<K,V>, Cloneable, java.io.Serializable 
    原文作者:风中孤寂
    原文地址: https://blog.csdn.net/qq_40870589/article/details/89067850
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞