各集合类区别与实现原理

HashMap和Hashtable的区别
1.两者最主要的区别在于Hashtable是线程安全,而HashMap则非线程安全
2.HashMap可以使用null作为key,而Hashtable则不允许null作为key(HashMap以null作为key时,总是存储在table数组的第一个节点上)
3.HashMap的初始容量为16,Hashtable初始容量为11,两者的填充因子默认都是0.75
4.HashMap扩容时是当前容量翻倍即:capacity*2,Hashtable扩容时是容量翻倍+1即:capacity*2+1
5.两者计算hash的方法不同
Hashtable计算hash是直接使用key的hashcode对table数组的长度直接进行取模
HashMap计算hash对key的hashcode进行了二次hash,以获得更好的散列值,然后对table数组长度取模


除开HashMap和Hashtable外,还有一个hash集合HashSet。
区别是HashSet不是key value结构,仅仅是存储不重复的元素,相当于简化版的HashMap,只是包含HashMap中的key而已。

HashSet的底层结构是哈希表,HashSet集合是通过元素中继承自Object超类的hashCode()方法和equal()方法来判断两个对象是否相同的。通过hashCode方法可以避免每次添加都需要equals的繁琐过程。所以我们在自己定义对象的时候,可以覆写对象继承自Object的这两个方法,使他们按照我们的意志来判断两个元素是否是同一元素。

TreeSet集合的底层是二叉树数据结构。他不仅不允许相同元素存在,更可以帮我们排序。我们将元素存入TreeSet集合之后他是按照自然顺序排序的。而我们要想让元素按照我们的意志进行排序,让元素实现Comparable接口,然后实现里面的CompareTo方法,返回0则代表元素相同,否则根据正数或者负数来判断排列顺序 TreeSet是非同步的

HashMap和Hashtable的实现原理
HashMap和Hashtable的底层实现都是数组+链表/红黑树结构实现的,当某个位桶的链表的长度达到某个阀值的时候,这个链表就将转换成红黑树,红黑树的 时间复杂度是logN。(当同一个hash值的节点数不小于8时,这就是JDK7与JDK8中HashMap实现的最大区别。)treeifyBin()就是将链表转换成红黑树。
JDK中Entry的名字变成了Node,原因是和红黑树的实现TreeNode相关联。
添加、删除、获取元素时都是先计算hash,根据hash和table.length计算index也就是table数组的下标,然后进行相应操作,以map为例:put过程是先计算hash然后通过hash与table.length取摸计算index值,然后将key放到table[index]位置,当table[index]已存在其它元素时,会在table[index]位置形成一个链表,将新添加的元素放在table[index],新添加的元素放在table的第一位,原来的元素通过Entry的next进行链接,这样以链表形式解决hash冲突问题,当元素数量达到临界值(capactiy*factor)时,则进行扩容,是table数组长度变为table.length*2
get的过程是先计算hash然后通过hash与table.length取摸计算index值,然后遍历table[index]上的链表,直到找到key,然后返回
(如果两个对象hashCode相同,会用hashCode找到bucket位置,然后调用key.equals()方法找到链表中正确的节点.最终找到要找的值对象。)

ArrayList:底层数据结构使数组结构,查询速度快,增删改慢,初始容量为10
当元素超出数组内容,会产生一个新数组,按照原数组的50%来延长,将原来数组的数据复制到新数组中,再将新的元素添加到新数组中。
浪费空间

LinkList:LinkedList底层的数据结构是基于双向循环链表的,且头结点中不存放数据,增删速度快,查询稍慢(二分查找法);
能克隆,支持序列化,是非同步的。是通过一个计数索引值来实现的。
例如,当我们调用get(int location)时,首先会比较“location”和“双向链表长度的1/2”;若前者大,则从链表头开始往后查找,直到location位置;否则,从链表末尾开始先前查找,直到location位置。


Vector:底层是数组结构,线程同步ArrayList是线程不同步;会new一个100%的浪费内存;
 


ConcurrentHashMap实现原理1.7
ConcurrentHashMap允许多个修改操作并发进行(16),其关键在于使用了锁分离技术。
它使用了多个锁来控制对hash表的不同段进行的修改,每个段其实就是一个小的hashtable,它们有自己的锁。只要多个并发发生在不同的段上,它们就可以并发进行。
ConcurrentHashMap在底层将key-value当成一个整体进行处理,这个整体就是一个Entry对象,key所对应的segment去做
与HashMap不同的是,ConcurrentHashMap使用多个子Hash表,也就是段(Segment)ConcurrentHashMap完全允许多个读操作并发进行,读操作并不需要加锁。

https://my.oschina.net/hosee/blog/639352
有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁。这里“按顺序”是很重要的,否则极有可能出现死锁,在ConcurrentHashMap内部,段数组是final的,并且其成员变量实际上也是final的,但是,仅仅是将数组声明为final的并不保证数组成员也是final的,这需要实现上的保证。这可以确保不会出现死锁,因为获得锁的顺序是固定的。

Segment下面包含很多个HashEntry列表数组。对于一个key,需要经过三次hash操作,才能最终定位这个元素的位置
这三次hash分别为:
1.对于一个key,先进行一次hash操作,得到hash值h1,也即h1 = hash1(key);
2.将得到的h1的高几位进行第二次hash,得到hash值h2,也即h2 = hash2(h1高几位),通过h2能够确定该元素的放在哪个Segment;
3.将得到的h1进行第三次hash,得到hash值h3,也即h3 = hash3(h1),通过h3能够确定该元素放置在哪个HashEntry。

concurrencyLevel表示并发级别,这个值用来确定Segment的个数,Segment的个数是大于等于concurrencyLevel的第一个2的n次方的数。
初始化过程
1.验证参数合法性
2.计算Segment数
3.然后使用循环找到大于等于concurrencyLevel的第一个2的n次方的数ssize,这个数就是Segment数组的大小,并记录一共向左按位移动的次数sshift,并令segmentShift = 32 - sshift,
  并且segmentMask的值等于ssize - 1,segmentMask的各个二进制位都为1,目的是之后可以通过key的hash值与这个值做&运算确定Segment的索引。
4.检查给的容量值是否大于允许的最大容量值,如果大于该值,设置为该值。最大容量值为static final int MAXIMUM_CAPACITY = 1 << 30;。
5.然后计算每个Segment平均应该放置多少个元素,这个值c是向上取整的值。(int c = initialCapacity / ssize;)比如初始容量为15,Segment个数为4,则每个Segment平均需要放置4个元素。
6.最后创建一个Segment实例,将其当做Segment数组的第一个元素。


put操作是要加锁的。操作步骤如下:
1.判断value是否为null,如果为null,直接抛出异常。
2.通过key进行两次hash确定应该去哪个Segment中放数据
5.向这个Segment对象中put值,这个put操作也基本是一样的步骤(通过&运算获取HashEntry的索引,然后set)。

get操作是不需要加锁的(如果value为null,会调用readValueUnderLock,只有这个步骤会加锁),通过volatile和final来确保数据安全。
1.和put操作一样,先通过key进行两次hash确定应该去哪个Segment中取数据。
2.使用Unsafe获取对应的Segment,然后再进行一次&运算得到HashEntry链表的位置,然后从链表头开始遍历整个链表(因为Hash可能会有碰撞,所以用一个链表保存),如果找到对应的key,则返回对应的value值,如果链表遍历完都没有找到对应的key,则说明Map中不包含该key,返回null。

size/containsValue:
先给3次机会,不lock所有的Segment,遍历所有Segment,累加各个Segment的大小得到整个Map的大小,如果某相邻的两次计算获取的所有Segment的更新的次数(modCount)是一样的,说明计算过程中没有更新操作,则直接返回这个值。如果这三次不加锁的计算过程中Map的更新次数有变化,则之后的计算先对所有的Segment加锁,再遍历所有Segment计算Map大小,最后再解锁所有Segment。

ConcurrentHashMap中的key和value值都不能为null,HashMap中key可以为null,HashTable中key不能为null。
ConcurrentHashMap是线程安全的类并不能保证使用了ConcurrentHashMap的操作都是线程安全的!
ConcurrentHashMap的get操作不需要加锁,put操作需要加锁


ConcurrentHashMap实现原理1.8

sizeCtl
负数代表正在进行初始化或扩容操作
-1代表正在初始化
-N 表示有N-1个线程正在进行扩容操作
正数或0代表hash表还没有被初始化,这个数值表示初始化或下一次进行扩容的大小,这一点类似于扩容阈值的概念。还后面可以看到,它的值始终是当前ConcurrentHashMap容量的0.75倍,这与loadfactor是对应的。


Node是最核心的内部类,它包装了key-value键值对,所有插入ConcurrentHashMap的数据都包装在这里面。它与HashMap中的定义很相似,但是但是有一些差别它对value和next属性设置了volatile同步锁(与JDK7的Segment相同),它不允许调用setValue方法直接改变Node的value域,它增加了find方法辅助map.get()方法。
当链表长度过长的时候,会转换为TreeNode。但是与HashMap不相同的是,它并不是直接转换为红黑树,而是把这些结点包装成TreeNode放在TreeBin对象中,由TreeBin完成对红黑树的包装。而且TreeNode在ConcurrentHashMap集成自Node类,而并非HashMap中的集成自LinkedHashMap.Entry<K,V>类,也就是说TreeNode带有next指针,这样做的目的是方便基于TreeBin的访问。
ForwardingNode:
一个用于连接两个table的节点类。它包含一个nextTable指针,用于指向下一张表。而且这个节点的key value next指针全部为null,它的hash值为-1. 这里面定义的find的方法是从nextTable里进行查询节点,而不是以自身为头节点进行查找。


对于ConcurrentHashMap来说,调用它的构造方法仅仅是设置了一些参数而已。而整个table的初始化是在向ConcurrentHashMap中插入元素的时候发生的。如调用put、computeIfAbsent、compute、merge等方法的时候,调用时机是检查table==null。
初始化方法主要应用了关键属性sizeCtl 如果这个值〈0,表示其他线程正在进行初始化,就放弃这个操作。在这也可以看出ConcurrentHashMap的初始化只能由一个线程完成。如果获得了初始化权限,就用CAS方法将sizeCtl置为-1,防止其他线程进入。初始化数组后,将sizeCtl的值改为0.75*n。

待补充

猜你喜欢

转载自blog.csdn.net/orzMrXu/article/details/102625132
今日推荐