HashMap,Hashtable以及ConcurrentHashMap的比较(源码)

一、概述

以前学习的时候应该都知道HashMap以及Hashtable:

HashMap是线程不安全的,Hashtable是线程安全的。

这里就一源代码的角度看看为什么Hashtable是线程安全的,以及另外一个线程安全的ConcurrentHashMap与Hashtable的比较。

小提示:在Ecilpse中可以用ctrl+shitf+T查找类,这样就容易查看源代码了。

在查看hashtable源代码的时候,不小心进了com.sun.org.apache.xalan.internal.xsltc.runtime下的Hashtable,看了半天不对劲,上网查了才发现原来是进错类了。

这里使用的是jdk8版本的源代码

二、Hashtable的线程安全

打开Hashtable的源代码,发现和HashMap几乎是一模一样的。

区别就在很多方法上都加上了"synchronized"关键字。

"synchronized"关键字的意思就是加锁了,不管是put,get还是什么的,统一加上了锁。

 public synchronized V get(Object key)
 public synchronized V put(K key, V value) 

那来分析一下这个锁,关键字在方法上,还不是静态方法,那锁的就是this,也就是当前对象。

就是说,不管谁来调用,只要是涉及到本对象的操作,不管增删改,锁的监视器都一样:this对象。

轮到的执行,没轮到的在外面排队等待。

也正是因为如此,Hashtable是线程安全的。(全加锁了,能不安全吗)

不管效率的话,显得有些低了。因为这里不管读写都加锁,而且锁的对象都一样,是整个对象。出现锁竞争与等待的可能很大。

三、ConcurrentHashMap的线程安全

1.先来看一下get方法:

	public V get(Object key) {
		//
        Node<K,V>[] tab;
		Node<K,V> e, p;
		int n, eh; K ek;
		//spread相当于HashMap中的hash方法,处理一下hash值,使其分布不容易碰撞
        int h = spread(key.hashCode());

		//table是当前对象中用于保存所有元素的数组,必须不能为空,而且长度大于0
		//tabAt的意思就是从table中找到hash为h的这个元素,如果没找到,说明不含有key为此值的元素
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
			//如果hash值相同,说明几乎就是
            if ((eh = e.hash) == h) {
				//再判断一下key是不是相同啊,是不是equals啊什么的,就准备返回了
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
			//这里跟下面的一样,都是遍历链表
            else if (eh < 0)
                return (p = e.find(h, key)) != null ? p.val : null;
			//既然在当前位置,第一个元素还不是,那就遍历这条链表,找到对应的
            while ((e = e.next) != null) {
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        return null;
    }

tabAt的具体实现如下,这里U对象是Unsafe类型的,其实就是通过操作内存,偏移来找到这个元素的所在位置。

  static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
        return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
    }

好像并不涉及到锁的操作。

2.再来看一下put方法

 public V put(K key, V value) {
        return putVal(key, value, false);
    }
 final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());

        int binCount = 0;
	//1.for
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f;
			int n, i, fh;
			//table还是空的情况,初始化
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
			//找到元素的位置是空的,直接放进去,下面的注释也说到了,不用锁。
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
			//跳过
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
				//到了这里,应该是找到了新的元素应该存放的位置,而且这个位置上还有其他的元素
                V oldVal = null;
				//此处加锁,锁的是f,f是什么?是找到的位于数组上的该位置上的第一个元素
                synchronized (f) {
					//之前f=tabat(tab,i),这里看来应该是必须成立的,直接下一步
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            binCount = 1;
							//然后就是寻找,替换。
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        else if (f instanceof TreeBin) {
                            Node<K,V> p;
                            binCount = 2;
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
				//替换了后,binCount是有所增加了,所以进入,并且在if的最后跳出循环。
                if (binCount != 0) {
					//就是查看下链表够不够长,需要换成红黑树不
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }

这里用到了一个锁,锁的是一个对象,是找到该元素对应的数组的位置上的那个元素。

也的确,对数组其他位置上进行操作的时候,与该操作是完全没影响的(扩容除外)。

所以说,ConcurrentHashMap的性能要比Hashtable的性能要好,支持多线程同时进行增加,查找等操作(只要hash定的index不一样,且不扩容。)

下面,再来看看扩容怎么写的

这个方法2个参数,第一个是增加了几个,第二个是binCount,链表的长度。

  private final void addCount(long x, int check) {
        CounterCell[] as; long b, s;
        if ((as = counterCells) != null ||
            !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
            CounterCell a; long v; int m;
            boolean uncontended = true;
            if (as == null || (m = as.length - 1) < 0 ||
                (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
                !(uncontended =
                  U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
                fullAddCount(x, uncontended);
                return;
            }
            if (check <= 1)
                return;
            s = sumCount();
        }
        if (check >= 0) {
            Node<K,V>[] tab, nt; int n, sc;
            while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
                   (n = tab.length) < MAXIMUM_CAPACITY) {
                int rs = resizeStamp(n);
                if (sc < 0) {
                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                        sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                        transferIndex <= 0)
                        break;
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                        transfer(tab, nt);
                }
                else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                             (rs << RESIZE_STAMP_SHIFT) + 2))
                    transfer(tab, null);
                s = sumCount();
            }
        }
    }

这个太复杂了。。就看compareAndSwap把。

unsafe.compareAndSwap就是拿着手上的去跟内存的对比,如果是一样的,那就没错,换上新的。

如果是不一样的,就说明其他线程改过了,你这个无效。你得重新来。(跟svn好像。。。)

所以在u.compareAndSwap的调用的时候一般放在死循环中,成功了再break。

至于在putVal的1.for处的那个for循环,我也不知道是干嘛的,我猜是以后把对单个元素加锁也换成u.compareAndSwap做准备的(纯属乱猜)

四、总结

如果是单线程情况,就用HashMap。多线程,还是用ConcurrentHashMap比较好。

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2024-10-13 16:46:19

HashMap,Hashtable以及ConcurrentHashMap的比较(源码)的相关文章

Java中HashMap底层实现原理(JDK1.8)源码分析

这几天学习了HashMap的底层实现,但是发现好几个版本的,代码不一,而且看了Android包的HashMap和JDK中的HashMap的也不是一样,原来他们没有指定JDK版本,很多文章都是旧版本JDK1.6.JDK1.7的.现在我来分析一哈最新的JDK1.8的HashMap及性能优化. 在JDK1.6,JDK1.7中,HashMap采用位桶+链表实现,即使用链表处理冲突,同一hash值的链表都存储在一个链表里.但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效

HashMap实现原理(jdk1.7),源码分析

HashMap实现原理(jdk1.7),源码分析 ? HashMap是一个用来存储Key-Value键值对的集合,每一个键值对都是一个Entry对象,这些Entry被以某种方式分散在一个数组中,这个数组就是HashMap的主干. 一.几大常量 //默认容量 16 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //最大容量 static final int MAXIMUM_CAPACITY = 1 << 30; //默认负载因

HashMap、HashTable、HashTree 深入分析及源码解析

在Java的集合中Map接口的实现实例中用的比较多的就是HashMap,今天我们一起来学学HashMap,顺便学学和他有关联的HashTable.HashTree 一.HashMap 1.基于哈希表的 Map 接口的实现.此实现提供所有可选的映射操作,并允许使用 null 值和 null 键.(除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同.)此类不保证映射的顺序,特别是它不保证该顺序恒久不变. 2.HashMap 的实例有两个参数影响其性能:初始容量

ConcurrentHashMap与synchronizedMap源码解析

一.synchronizedMap Collections.synchronized*(m)将线程不安全集合变为线程安全集合,从源码来看由于synchronizedMap的作用就是将Map的各种方法添加了synchronized关键字进行修饰的. 1 private static class SynchronizedMap<K,V> 2 implements Map<K,V>, Serializable { 3 private static final long serialVer

JDK中ArrayList、HashMap和HashSet的equals方法源码分析

最近遇到个坑,在分别对ArrayList.HashMap等数据类型进行比较时,发现数据一样,但equals一直返回false.于是乎看了一下ArrayList和HashMap的源码,才恍然大悟.本文的代码摘自JDK 1.7.0. ArrayList的equals方法: public boolean equals(Object o) { if (o == this) return true; if (!(o instanceof List)) return false; ListIterator<

Java集合:HashMap底层实现和原理(源码解析)

Note:文章的内容基于JDK1.7进行分析.1.8做的改动文章末尾进行讲解. 一.先来熟悉一下我们常用的HashMap: 1.概述 HashMap基于Map接口实现,元素以键值对的方式存储,并且允许使用null 建和null 值, 因为key不允许重复,因此只能有一个键为null,另外HashMap不能保证放入元素的顺序,它是无序的,和放入的顺序并不能相同.HashMap是线程不安全的. 2.继承关系 public class HashMap<K,V>extends AbstractMap&

【转】【java源码分析】Map中的hash算法分析

全网把Map中的hash()分析的最透彻的文章,别无二家. 2018年05月09日 09:08:08 阅读数:957 你知道HashMap中hash方法的具体实现吗?你知道HashTable.ConcurrentHashMap中hash方法的实现以及原因吗?你知道为什么要这么实现吗?你知道为什么JDK 7和JDK 8中hash方法实现的不同以及区别吗?如果你不能很好的回答这些问题,那么你需要好好看看这篇文章.文中涉及到大量代码和计算机底层原理知识.绝对的干货满满.整个互联网,把hash()分析的

Java集合专题总结(1):HashMap 和 HashTable 源码学习和面试总结

2017年的秋招彻底结束了,感觉Java上面的最常见的集合相关的问题就是hash--系列和一些常用并发集合和队列,堆等结合算法一起考察,不完全统计,本人经历:先后百度.唯品会.58同城.新浪微博.趣分期.美团点评等都在1.2--面的时候被问过无数次,都问吐了&_&,其他公司笔试的时候,但凡有Java的题,都有集合相关考点,尤其hash表--现在总结下. Java集合概述 HashMap介绍 HashMap源码学习 关于HashMap的几个经典问题 HashTable介绍和源码学习 Hash

从Java源码的角度来分析HashMap与HashTable的区别

由于HashMap与HashTable都是用来存储Key-Value的键值对,所以经常拿来对比二者的区别,下面就从源码的角度来分析一下HashMap与HashTable的区别, 首先介绍一下两者的区别,然后再从源码分析. HahMap与HahTable两者主要区别: 1.继承的父类不同 <span style="font-size:18px;">public class HashMap<K, V> extends AbstractMap<K, V>