ConcurrentHashMap源码分析
来源:互联网 发布:中国历史教科书知乎 编辑:程序博客网 时间:2024/05/16 02:46
前言
ConcurrentHashMap是concurrent包里面出镜率很高的一个类,这个类是线程安全的Map,原来jdk1.8以前的ConcurrentHashMap采用的是锁分段机制来保证线程安全。
如果关注这个锁分段技术的,可以参考这篇博文:
http://blog.csdn.net/yansong_8686/article/details/50664351
至于我,可能就不详细说锁分段机制了,这篇文章主要针对的是JDK1.8中的ConcurrentHashMap的实现方法,锁分段技术,作为一种思想,我会简单说说我的理解,我也不知道全不全面,正不正确,也是一种理解吧。
先说说锁分段技术吧
先从一个问题说起,常常在面试中,都会问有关于HashMap的相关问题:HashTable和HashMap的区别是什么?
答案一般人可能会回答HashTable是线程安全的,HashMap不是线程安全的。
OK,这没错,那HashTable现在还在用吗?
不再用了。
为什么呢?
效率低。
效率怎么低了?
如果查看底层源码的时候,就会发现,它的多线程同步处理,简直简单粗暴。就是把所有的Map中的方法加上synchronized关键字。这样一来有什么问题呢?
一个线程正在做一个什么样的操作的话,其他的N个线程都要等待,这样的多线程在线程数非常大的情况下,根本就运转不开。那咋办?除了我们自己写,在jdk1.5之后, 出现的ConcurrentHashMap就提供的一种解决思路。
可以看到,下面这张图的话,就是1.8版本以前的分段加锁的机制, 这里的ConcurrentHashMap是分成了N个Segment,这个就是片段。每个Segment又有HashEntry的数组,数组又挂着链。因此,如果加锁的话,其实也就是加一个Segment的锁,对其他的Segment没有影响。这样的做法呢,实际上是把加锁的粒度变小了。锁还是加的,还是不是全加,加一部分。
JDK1.8有什么改变
从结构图上看,它放弃了原有的Segment的结构,直接用Entry数组的方式。
当然这个图,也不是特备完整,因为在JDK1.8中,当链表长度超过8的时候,链表会转换成红黑树。当然,这不在我们的ConcurrentHashMap的讨论范围之中,我们还是主要讨论线程安全怎么保证。
要探究,底层源码跑不掉
// 截取部分的ConcurrentHashMap的源码分析// put方法的底层是putVal方法public V put(K key, V value) { return putVal(key, value, false);}// 这个方法是不能被overridefinal V putVal(K key, V value, boolean onlyIfAbsent) { // ConcurrentHashMap是不能put null的 if (key == null || value == null) throw new NullPointerException(); // 得到key对应的hash位置 int hash = spread(key.hashCode()); // 这个主要看到底一个index下挂的有几个Node,初始当然是没有Node int binCount = 0; // 无线循环 for (Node<K,V>[] tab = table;;) { Node<K,V> f; int n, i, fh; // 如果这个table都还没有初始化过 if (tab == null || (n = tab.length) == 0) // 初始化table tab = initTable(); // 如果index=hash%n的那个位置Node是空 else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { // 就把新的Node放到table的那个index处,如果是把新Node放到null的index处,不加锁。 if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null))) break; // no lock when adding to empty bin } // 如果取出的那个Node正在移动,那就帮助扩容 else if ((fh = f.hash) == MOVED) tab = helpTransfer(tab, f); // 否则就是说呢hash为index的这个地方本来就有Node,到底是修改值,还是加链表 else { V oldVal = null; // 在这个地方加了锁的操作,这个粒度是table中的某个index取出来的Node,粒度很小。 synchronized (f) { // 下面的这两个if就是再次判断的作用,是不是那个Node,是不是那个hash值 if (tabAt(tab, i) == f) { if (fh >= 0) { // 这个时候把binCount置为1,因为有一个Node嘛 binCount = 1; // 这一步可以得出这个链表有多长 for (Node<K,V> e = f;; ++binCount) { K ek; // 如果遍历的时候,找到了我们传入的key,已经在链表里有了 if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) { // value赋为新值,并且跳出循环 oldVal = e.val; if (!onlyIfAbsent) e.val = value; break; } Node<K,V> pred = e; // 如果遍历完了还没找到 if ((e = e.next) == null) { // 把value变成链表的下一个Node,并跳出循环 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; } } } } // 如果这个链表的Node的个数大于8了,转成红黑树 if (binCount != 0) { if (binCount >= TREEIFY_THRESHOLD) treeifyBin(tab, i); if (oldVal != null) return oldVal; break; } } } // 这个方法主要做了两件事,第一Map中的元素个数+1,第二判断需不需要扩容,如果需要,扩容。 addCount(1L, binCount); return null; }
初始化table
private final Node<K,V>[] initTable() { Node<K,V>[] tab; int sc; while ((tab = table) == null || tab.length == 0) { /** * SizCtl的解释 * Table initialization and resizing control. When negative, the * table is being initialized or resized: -1 for initialization, * else -(1 + the number of active resizing threads). Otherwise, * when table is null, holds the initial table size to use upon * creation, or 0 for default. After initialization, holds the * next element count value upon which to resize the table. */ // sc<0时,线程暂停,让CPU重新选择 if ((sc = sizeCtl) < 0) Thread.yield(); // lost initialization race; just spin // 如果>=0,看过我Atomic类介绍的亲是不是很熟悉这个方法,正式CAS方法实现同步 else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { try { // 再三确定table是空的 if ((tab = table) == null || tab.length == 0) { int n = (sc > 0) ? sc : DEFAULT_CAPACITY; @SuppressWarnings("unchecked") // 按照初始值16去创建Node数组 Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n]; table = tab = nt; // 此时sc=12 sc = n - (n >>> 2); } } finally { // sizeCtl=12,说明下次在size=12的时候,resize这个数组 sizeCtl = sc; } break; } } // 返回initial之后的table return tab; }
get方法
public V get(Object key) { Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek; //计算hash值 int h = spread(key.hashCode()); //根据hash值确定节点位置 if ((tab = table) != null && (n = tab.length) > 0 && (e = tabAt(tab, (n - 1) & h)) != null) { //如果搜索到的节点key与传入的key相同且不为null,直接返回这个节点 if ((eh = e.hash) == h) { if ((ek = e.key) == key || (ek != null && key.equals(ek))) return e.val; } //如果eh<0 说明这个节点在树上 直接寻找 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; }
后记
这篇文章主要简单说了ConcurrentHashMap的安全机制,对size扩容机制并没有做详细的介绍,如果想进一步了解的朋友,可以看看参考文献中的文章,写的很好,受益良多。
参考文献
http://www.jianshu.com/p/e694f1e868ec
http://blog.csdn.net/u010723709/article/details/48007881
http://blog.csdn.net/yansong_8686/article/details/50664351
- ConcurrentHashMap 源码分析 (二)
- ConcurrentHashMap源码分析
- ConcurrentHashMap 源码分析
- ConcurrentHashMap源码分析--Java8
- Java源码分析:ConcurrentHashMap
- Java-ConcurrentHashMap源码分析
- ConcurrentHashMap之源码分析
- ConcurrentHashMap源码分析
- 集合源码分析----ConcurrentHashMap
- ConcurrentHashMap源码分析--Java8
- 源码分析-ConcurrentHashMap
- ConcurrentHashmap源码分析(jdk7)
- ConcurrentHashMap源码分析
- Hashtable、ConcurrentHashMap源码分析
- ConcurrentHashMap源码分析1
- ConcurrentHashMap源码分析
- ConcurrentHashMap源码分析1.8
- ConcurrentHashMap源码分析
- Java设计模式之——单例模式
- Mysql的编译安装
- React Native之环境搭建
- hdu 1264 counting the squares
- struts2 下载问题java.lang.ClassCastException: java.io.ByteArrayInputStream cannot be cast to
- ConcurrentHashMap源码分析
- 由于之前电脑有问题,今天终于配置好了myeclipse
- windows10更新后的不过 网络连接现实感叹号
- hive 之 lateral view
- ORACLE查看表空间的名称及大小
- NFS(网络文件系统)服务器搭建及挂载
- php 在类中使用静态方法的几种方式
- php 判断上传文件类型 $_files[]['type']值大全
- Struts加载时机和加载顺序