[java] java容器

前言

各种知识多而且容易遗忘,还不容易复习。最好的方法当然是自己给自己提问,不断补缺查漏,缺什么补什么。本文将各类知识归类,并将全文知识点浓缩在自问自查中,并且都写好目录,自问自查时可以随时跳转过去,方便大家系统的学习复习知识。 水平有限,有错误敬请指正

食用方法
自问自查—阅读原文—自问自查–阅读原文…
无限循环


自查自问

1. ArrayList 和 vector 
2. CopyOnWriteArrayList 原理
3. HashTable 
4. 散列和链地址
5. HashMap  get  put  死循环
6. ConcurrentHashMap
7. LinkedHashMap、TreeMap 实现原理


关系图

在这里插入图片描述
AbstractMap 这种框抽象类Map 这种接口

接口可以继承接口

1.继承关系

继承关系使用如下箭头:
在这里插入图片描述

由子类指向父类。

2.实现关系

实现关系使用如下箭头:
在这里插入图片描述

有实现类指向接口

3.依赖关系

依赖关系使用如下箭头:
在这里插入图片描述

由使用者指向被使用者。

如果A指向B,则说明A中使用了B,使用方式包括A类中有B类实例化对象的局部变量。A类中有方法把B类实例化对象当做了参数,A类中有方法调用了B类中的静态方法。

ArrayList

https://blog.csdn.net/qq_35190492/article/details/103883964
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

CopyOnWriteArrayList

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

HashTable

HashMap可以为空 Hashtable和currentHashMap不能为空

Hashtable在对数据操作的时候都会上锁,所以效率比较低下。
在这里插入图片描述
在这里插入图片描述

散列优缺点

链地址法的优点
与开放定址法相比,拉链法有如下几个优点:
①链地址法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;

②由于链地址法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况;

③开放定址法为减少冲突,要求装填因子α较小,故当结点规模较大时会浪费很多空间。而链地址法中可取α≥1,且结点较大时,链地址法中增加的指针域可忽略不计,因此节省空间;

④在用链地址法构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。而对开放地址法构造的散列表,删除结点不能简单地将被删结 点的空间置为空,否则将截断在它之后填人散列表的同义词结点的查找路径。这是因为各种开放地址法中,空地址单元(即开放地址)都是查找失败的条件。因此在 用开放地址法处理冲突的散列表上执行删除操作,只能在被删结点上做删除标记,而不能真正删除结点。

链地址法的缺点
 链地址法的缺点是:指针需要额外的空间,故当结点规模较小时,开放定址法较为节省空间,而若将节省的指针空间用来扩大散列表的规模,可使装填因子变小,这又减少了开放定址法中的冲突,从而提高平均查找速度。

https://blog.csdn.net/ldw662523/article/details/79567817

HashMap

从结构实现来讲,HashMap是数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的,如下如所示。
默认 16
在这里插入图片描述

(1) 从源码可知,HashMap类中有一个非常重要的字段,就是 Node[] table,即哈希桶数组,明显它是一个Node的数组。我们来看Node[JDK1.8]是何物。

static class Node<K,V> implements Map.Entry<K,V> { final int hash; //用来定位数组索引位置 final K key; V value; Node<K,V> next; //链表的下一个node Node(int hash, K key, V value, Node<K,V> next) { ... } public final K getKey(){ ... }public final V getValue() { ... } public final String toString() { ... } public final int hashCode() { ... } public final V setValue(V newValue) { ... } public final boolean equals(Object o) { ... } }

Node是HashMap的一个内部类,实现了Map.Entry接口,本质是就是一个映射(键值对)。上图中的每个黑色圆点就是一个Node对象。
Node[] table的初始化长度length(默认值是16),Load factor为负载因子(默认值是0.75) 超过就扩容两倍。

功能实现-方法1. 确定哈希桶数组索引位置主要是从速度,功效和质量来考虑的,减少系统的开销,也不会造成因为高位没有参与下标的计算,从而引起的碰撞。

在这里插入图片描述
桶的容量是2倍数所以 length-1 得到的都是1,当length总是2的n次方时,h& (length-1)运算等价于对length取模,也就是h%length,但是&比%具有更高的效率。
在这里插入图片描述
2. 分析HashMap的put方法
在这里插入图片描述
1.8 源码
在这里插入图片描述
在这里插入图片描述
3. 扩容机制我们分析下resize的源码,鉴于JDK1.8融入了红黑树,较复杂,为了便于理解我们仍然使用JDK1.7的代码,好理解一些,本质上区别不大,具体区别后文再说。 扩容两倍

在这里插入图片描述
这里就是使用一个容量更大的数组来代替已有的容量小的数组,transfer()方法将原有Entry数组的元素拷贝到新的Entry数组里。

在这里插入图片描述
newTable[i]的引用赋给了e.next,也就是使用了单链表的头插入方式,同一位置上新元素总会被放在链表的头部位置;这样先放在一个索引上的元素终会被放到Entry链的尾部(如果发生了hash冲突的话),这一点和Jdk1.8有区别,下文详解。在旧数组中同一条Entry链上的元素,通过重新计算索引位置后,有可能被放到了新数组的不同位置上。
下面举个例子说明下扩容过程。假设了我们的hash算法就是简单的用key mod 一下表的大小(也就是数组的长度)。其中的哈希桶数组table的size=2, 所以key = 3、7、5,put顺序依次为 5、7、3。在mod 2以后都冲突在table[1]这里了。这里假设负载因子 loadFactor=1,即当键值对的实际大小size 大于 table的实际大小时进行扩容。接下来的三个步骤是哈希桶数组 resize成4,然后所有的Node重新rehash的过程。
在这里插入图片描述
java 1.8会在链表的尾部插入
下面我们讲解下JDK1.8做了哪些优化。经过观测可以发现,我们使用的是2次幂的扩展(指长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动一个原数组长度的位置。
只需要看看原来的hash值新增的那个bit是1还是0就好了(和老数组长度取& 老数组2的倍数 0010000形势),是0的话索引没变,是1的话索引变成“原索引+oldCap”,可以看看下图为16扩充为32的resize示意图:

在这里插入图片描述

并发HashMap可能造成死循环
在这里插入图片描述
其中,map初始化为一个长度为2的数组,loadFactor=0.75,threshold=2*0.75=1,也就是说当put第二个key的时候,map就需要进行resize。
通过设置断点让线程1和线程2同时debug到transfer方法的首行。注意此时两个线程已经成功添加数据。放开thread1的断点至transfer方法的“Entry next = e.next;” 这一行;然后放开线程2的的断点,让线程2进行resize。结果如下图。
在这里插入图片描述
注意,Thread1的 e 指向了key(3),而next指向了key(7),其在线程二rehash后,指向了线程二重组后的链表。
线程一被调度回来执行,先是执行 newTalbe[i] = e, 然后是e = next,导致了e指向了key(7),而下一次循环的next = e.next导致了next指向了key(3)。
在这里插入图片描述
在这里插入图片描述
e.next = newTable[i] 导致 key(3).next 指向了 key(7)。注意:此时的key(7).next 已经指向了key(3), 环形链表就这样出现了。
在这里插入图片描述
于是,当我们用线程一调用map.get(11)时,悲剧就出现了——Infinite Loop。

https://tech.meituan.com/2016/06/24/java-hashmap.html

hash冲突:

拉链法

开放地址

平方探测再散列是加1的平方;减1的平方,加2的平方,减2的平方,加3的平方,减3的平方。。。加k的平方,减k的平方。

链地址法的缺点是:指针需要额外的空间,故当结点规模较小时,开放定址法较为节省空间

ConcurrentHashMap

ConcurrentHashMap使用分段锁技术,将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问,能够实现真正的并发访问。如下图是ConcurrentHashMap的内部结构图:
在这里插入图片描述
Segment下面包含很多个HashEntry列表数组。对于一个key,需要经过三次(为什么要hash三次下文会详细讲解)hash操作,才能最终定位这个元素的位置,这三次hash分别为:
对于一个key,先进行一次hash操作,得到hash值h1,也即h1 = hash1(key); 将得到的h1的高几位进行第二次hash,得到hash值h2,也即h2 = hash2(h1高几位),通过h2能够确定该元素的放在哪个Segment; 将得到的h1进行第三次hash,得到hash值h3,也即h3 = hash3(h1),通过h3能够确定该元素放置在哪个HashEntry。
ConcurrentHashMap中主要实体类就是三个:ConcurrentHashMap(整个Hash表),Segment 默认16(桶),HashEntry(节点)
ConcurrentHashMap中的key和value值都不能为null,HashMap中key可以为null,HashTable中key不能为null。 ConcurrentHashMap是线程安全的类并不能保证使用了ConcurrentHashMap的操作都是线程安全的!
数据结构方面都是采用了数组+链表+红黑树的结构。不同的是,CurrentHashMap很好的处理了并发的问题。相比于HashTable的整段加锁,CurrentHashMap更加地细化了。

put
前面的所有的介绍其实都为这个方法做铺垫。ConcurrentHashMap最常用的就是put和get两个方法。现在来介绍put方法,这个put方法依然沿用HashMap的put方法的思想,根据hash值计算这个新插入的点在table中的位置i,如果i位置是空的,直接放进去,否则进行判断,如果i位置是树节点,按照树的方式插入新的节点,否则把i插入到链表的末尾。ConcurrentHashMap中依然沿用这个思想,有一个最重要的不同点就是ConcurrentHashMap不允许key或value为null值。另外由于涉及到多线程,put方法就要复杂一点。在多线程中可能有以下两个情况:
*
如果一个或多个线程正在对ConcurrentHashMap进行扩容操作,当前线程也要进入扩容的操作中。这个扩容的操作之所以能被检测到,是因为transfer方法中在空结点上插入forward节点,如果检测到需要插入的位置被forward节点占有,就帮助进行扩容;
*
如果检测到要插入的节点是非空且不是forward节点,就对这个节点加锁,这样就保证了线程安全。尽管这个有一些影响效率,但是还是会比hashTable的synchronized要好得多。

ConcurrentHashMap
*
JDK 1.7 中使用分段锁(ReentrantLock + Segment + HashEntry),相当于把一个 HashMap 分成多个段,每段分配一把锁,这样支持多线程访问。锁粒度:基于 Segment,包含多个 HashEntry。
*
JDK 1.8 中使用 CAS + synchronized + Node + 红黑树。锁粒度:Node(首结
点)(实现 Map.Entry)。锁粒度降低了。
在这里插入图片描述

synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。
JDK8中的实现也是锁分离的思想,它把锁分的比segment 更细一些,只要hash不冲突,就不会出现并发获得锁的情况。它首先使用无锁操作CAS插入头结点,如果插入失败,说明已经有别的线程插入头结点了,再次循环进行操作。如果头结点已经存在,则通过synchronized获得头结点锁,进行后续的操作。

在这里插入图片描述

LinkedHashMap、TreeMap

LinkedHashMap

在LinkedHashMap中,是通过双联表的结构来维护节点的顺序的。每个节点都进行了双向的连接,维持插入的顺序(默认)。head指向第一个插入的节点,tail指向最后一个节点。在这里插入图片描述

TreeMap的内部类Entry,它有几个重要的属性:

//键
K key;
//值
V value;
//左孩子
Entry<K,V> left = null;
//右孩子
Entry<K,V> right = null;
//父亲
Entry<K,V> parent;
//颜色


自查自问

1. ArrayList 和 vector 
2. CopyOnWriteArrayList 原理
3. HashTable 
4. 散列和链地址
5. HashMap  get  put  死循环
6. ConcurrentHashMap
7. LinkedHashMap、TreeMap 实现原理
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值