HashMap,Hashtable以及ConcurrentHashMap的比较(源码)
2015-08-25 23:36
459 查看
一、概述
以前学习的时候应该都知道HashMap以及Hashtable:
HashMap是线程不安全的,Hashtable是线程安全的。
这里就一源代码的角度看看为什么Hashtable是线程安全的,以及另外一个线程安全的ConcurrentHashMap与Hashtable的比较。
小提示:在Ecilpse中可以用ctrl+shitf+T查找类,这样就容易查看源代码了。
在查看hashtable源代码的时候,不小心进了com.sun.org.apache.xalan.internal.xsltc.runtime下的Hashtable,看了半天不对劲,上网查了才发现原来是进错类了。
这里使用的是jdk8版本的源代码
预备:
unsafe.compareAndSwap();
比如说现在内存中有
a=1;
线程T1拿到a=1,然后准备修改a,这个时候就调用unsafe.compareAndSwap()去修改。
访问内存的时候:如果发现内存中的a还是T1认为的那样(为1),则修改,并且返回true.
如果内存中的a已经被其他线程修改了(跟手上的a的值对不上,不为1),则不修改,返回false;
所以一般在执行compareAndSwap()的时候,通常是放在一个死循环中:
取值->业务->赋值->发现值变了->重新再来直到成功
跟锁的区别:
锁是规定了只有一个线程能够进来处理,处理完了再出去。完全一条队排队处理。
而compareAndSwap()是去登记一下,取个值,根据这个值进行一系列操作,操作完了拿去提交,提交的时候检查一下值是否变了,如果变了,那就说刚才做的白做了。如果没变,那就是了,修改。其实就是svn,svn...
二、Hashtable的线程安全
打开Hashtable的源代码,发现和HashMap几乎是一模一样的。
区别就在很多方法上都加上了"synchronized"关键字。
"synchronized"关键字的意思就是加锁了,不管是put,get还是什么的,统一加上了锁。
那来分析一下这个锁,关键字在方法上,还不是静态方法,那锁的就是this,也就是当前对象。
就是说,不管谁来调用,只要是涉及到本对象的操作,不管增删改,锁的监视器都一样:this对象。
轮到的执行,没轮到的在外面排队等待。
也正是因为如此,Hashtable是线程安全的。(全加锁了,能不安全吗)
不管效率的话,显得有些低了。因为这里不管读写都加锁,而且锁的对象都一样,是整个对象。出现锁竞争与等待的可能很大。
三、ConcurrentHashMap的线程安全
1.先来看一下get方法:
tabAt的具体实现如下,这里U对象是Unsafe类型的,其实就是通过操作内存,偏移来找到这个元素的所在位置。
好像并不涉及到锁的操作。
2.再来看一下put方法
这里用到了一个锁,锁的是一个对象,是找到该元素对应的数组的位置上的那个元素。
也的确,对数组其他位置上进行操作的时候,与该操作是完全没影响的(扩容除外)。
所以说,ConcurrentHashMap的性能要比Hashtable的性能要好,支持多线程同时进行增加,查找等操作(只要hash定的index不一样,且不扩容。)
下面,再来看看扩容怎么写的
这个方法2个参数,第一个是增加了几个,第二个是binCount,链表的长度。
****
再回来看看那个for循环是干嘛的。
最前面说过一般把compareAndSwap是放到for循环中的,如果失败了就继续尝试。
所以说上面的tabAt方法多次调用是有原因的:检查内存中的值是否变了,如果变了,果断去要新的,放弃本次。
四、总结
如果是单线程情况,就用HashMap。多线程,还是用ConcurrentHashMap比较好。
以前学习的时候应该都知道HashMap以及Hashtable:
HashMap是线程不安全的,Hashtable是线程安全的。
这里就一源代码的角度看看为什么Hashtable是线程安全的,以及另外一个线程安全的ConcurrentHashMap与Hashtable的比较。
小提示:在Ecilpse中可以用ctrl+shitf+T查找类,这样就容易查看源代码了。
在查看hashtable源代码的时候,不小心进了com.sun.org.apache.xalan.internal.xsltc.runtime下的Hashtable,看了半天不对劲,上网查了才发现原来是进错类了。
这里使用的是jdk8版本的源代码
预备:
unsafe.compareAndSwap();
比如说现在内存中有
a=1;
线程T1拿到a=1,然后准备修改a,这个时候就调用unsafe.compareAndSwap()去修改。
访问内存的时候:如果发现内存中的a还是T1认为的那样(为1),则修改,并且返回true.
如果内存中的a已经被其他线程修改了(跟手上的a的值对不上,不为1),则不修改,返回false;
所以一般在执行compareAndSwap()的时候,通常是放在一个死循环中:
取值->业务->赋值->发现值变了->重新再来直到成功
跟锁的区别:
锁是规定了只有一个线程能够进来处理,处理完了再出去。完全一条队排队处理。
而compareAndSwap()是去登记一下,取个值,根据这个值进行一系列操作,操作完了拿去提交,提交的时候检查一下值是否变了,如果变了,那就说刚才做的白做了。如果没变,那就是了,修改。其实就是svn,svn...
二、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(); } } }
****
再回来看看那个for循环是干嘛的。
最前面说过一般把compareAndSwap是放到for循环中的,如果失败了就继续尝试。
所以说上面的tabAt方法多次调用是有原因的:检查内存中的值是否变了,如果变了,果断去要新的,放弃本次。
四、总结
如果是单线程情况,就用HashMap。多线程,还是用ConcurrentHashMap比较好。
相关文章推荐
- 【面经】百度商务搜索部一二面面经
- Cube Stacking
- 我的git 搭建
- 网络
- Android异步消息处理机制(4)AsyncTask源码解析
- MVC 定义JsonpResult实现跨域请求
- R语言-时间刻度的转换
- Audit Policy Recommendations
- 简单实现多线程数据共享
- 设计模式学习笔记十五:命令模式
- 链接属性和存储类型
- Linux系统磁盘管理基本知识
- 不带signed或unsigned关键字的char型 无符号数? 有符号数? C标准规定为 Implementation Defined !!!
- Effective C++——条款1和条款2(第1章)
- lesson2-java虚拟机之jvm结构
- GCD总结
- Tween动画
- CSS学习------之简单图片切换
- 大分享-hibernate,springmvc,easyui简要介绍
- C#解析HTML