PS:不得不说Java编程思想这本书是真心强大..算法
学习内容:编程
1.HashMap<K,V>在多线程的状况下出现的死循环现象api
当初学Java的时候只是知道HashMap<K,V>在并发的状况下使用的话,会出现线程安全问题,可是一直都没有进行深刻的研究,也是最近实验室的徒弟在问起这个问题的缘由以后,才开始进行了一个深刻的研究.安全
那么这一章也就仅仅针对这个问题来讲一下,至于如何使用HashMap这个东西,也就不进行介绍了.在面对这个问题以前,咱们先看一下HashMap<K,V>的数据结构,学过C语言的,你们应该都知道哈希表这个东西.其实HashMap<K,V>和哈希表我能够说,思想上基本都是同样的.数据结构
这就是两者的数据结构,上面那个是C语言的数据结构,也就是哈希表,下面的则是Java中HashMap<K,V>的数据结构,虽然数据结构上稍微有点差别,不过思想都是同样的.咱们仍是以HashMap<K,V>进行讲解,咱们知道HashMap<K,V>有一个叫装载因子的东西,默认状况下HashMap<K,V>的装载因子是75%这是在时间和空间上寻求的一个折衷.那么什么是所谓的装载因子,装载因子实际上是用来判断当前的HashMap<K,V>中存放的数据量,若是咱们存放的数据量大于了75%,那么HashMap<K,V>就须要进行扩容操做,扩容的空间大小就是原来空间的两倍.可是扩容的时候须要reshash操做,其实就是讲全部的数据从新计算HashCode,而后赋给新的HashMap<K,V>,rehash的过程是很是耗费时间和空间的,所以在咱们对HashMap的大小进行控制的时候,应该要进行至关的考虑.还有一个误区(HashMap<K,V>可不是无限大的.)多线程
简单介绍完毕以后,就说一下正题吧.其实在单线程的状况下,HashMap<K,V>是不会出现问题的.可是在多线程的状况下也就是并发状况下,就会出现问题.若是HashMap<K,V>的容量很大,咱们存入的数据不多,在并发的状况下出现问题的概率仍是很小的.出现问题的主要缘由就是,当咱们存入的数据过多的时候,尤为是须要扩容的时候,在并发状况下是很容易出现问题.针对这个现象,咱们来分析一下.并发
resize()函数..函数
void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } Entry[] newTable = new Entry[newCapacity]; boolean oldAltHashing = useAltHashing; useAltHashing |= sun.misc.VM.isBooted() && (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD); boolean rehash = oldAltHashing ^ useAltHashing; transfer(newTable, rehash); //transfer函数的调用 table = newTable; threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); }
上面说过,但HashMap<K,V>的空间不足的状况下,须要进行扩容操做,所以在Java JDK中须要使用resize()函数,Android api中是找不到resize函数的,Android api是使用ensureCapacity来完成调用的..原理其实都差很少,我这里仍是只说Java JDK中的..其实在resize()这个过程当中,在并发状况下也是不会出现问题的..
高并发
关键问题是transfer函数的调用过程..咱们来看一下transfer的源码..
学习
void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; for (Entry<K,V> e : table) { //这里才是问题出现的关键.. while(null != e) { Entry<K,V> next = e.next; //寻找到下一个节点.. if (rehash) { e.hash = null == e.key ? 0 : hash(e.key); } int i = indexFor(e.hash, newCapacity); //从新获取hashcode e.next = newTable[i]; newTable[i] = e; e = next; } } }
transfer函数实际上是在并发状况下致使死循环的因素..由于这里涉及到了指针的移动的过程..transfer的源码一开始我并有彻底的看懂,主要仍是newTable[i]=e的这个过程有点让人难理解..其实这个过程是一个很是简单的过程..咱们来看一下下面这张图片..
这是在单线程的正常状况下,当HashMap<K,V>的容量不够以后的扩容操做,将旧表中的数据赋给新表中的数据.正常状况下,就是上面图片显示的那样.新表的数据就会很正常,而且还须要说的一点就是,进行扩容操做以后,在旧表中key值相同的数据块在新表中数据块的链接方式会逆向.就拿key = 3和key = 7的两个数据块来讲,在旧表中是key = 3 的数据块指向key = 7的数据块的,可是在新表中,key = 7的数据块则是指向了key = 3的数据块key = 5 的数据块不和两者发生冲突,所以就保存到了 i = 1 的位置(这里的hash算法采用 k % hash.size() 的方式).这里采用了这样简单的算法无非是帮助咱们理解这个过程,固然在正常状况下算法是不可能这么简单的.
这样在单线程的状况下就完成了扩容的操做.其中不会出现其余的问题..可是若是是在并发的状况下就不同了.并发的状况出现问题会有不少种状况.这里我简单的说明俩种状况.咱们来看图。
这张图可能有点小,你们能够经过查看图像来放大,就可以看清晰内容了...
这张图说明了两种死循环的状况.第一种相对而严仍是很容易理解的.第二种可能有点费劲..可是有一点咱们须要记住,图中t1和t2拿到的是同一个内存单元对应的数据块.而不是t1拿到了一个独立的数据块,t2拿到了一个独立的数据块..这是不对的..之因此发生系循环的缘由就是由于拿到的数据块是同一个内存单元对应的数据块.这点咱们须要注意..正是由于在高并发的状况下线程的工做方式是不肯定的,咱们没法预知线程的工做状况.所以在高并发的状况下,咱们不要使用多线程对HashMap<K,V>进行操做,不然咱们都不知道究竟是哪里出了问题.
可能看起来很复杂,可是只要去思考,仍是感受蛮简单的,我这只是针对两个线程来分析了一下死循环的状况,固然发生死循环的问题不只仅只是这两种方式,方式可能会有不少,我这里只是针对了两个类型进行了分析,目的是方便你们理解.发生死循环的方式毫不仅仅只是这两种状况.至于其余的状况,你们若是愿意去了解,能够本身再去研磨研磨其余的方式.按照这种思路分析,仍是能研磨出来的.而且这仍是两个线程,若是数据量很是大,线程的使用还比较多,那么就更容易发生死循环的现象.所以这就是致使HashMap<K,V>在高并发下致使死循环的缘由.
虽然咱们都知道当多线程对Map进行操做的时候,咱们只须要使用ConcurrentHashMap<K,V>就能够了.可是咱们仍是须要知道为何HashMap<K,V>在高并发的状况下不可以那样去使用.学同样东西,不只仅要知道,并且还要知道其中的缘由和道理.