吗 Map
public interface Map<K,V> 两个泛型 ,一个键,一个值
将键映射到值的对象。一个映射不能包含重复的键;每个键最多只能映射到一个值。
Collection<E> Map<K,v>
单列集合 双列集合
每次只能保存一个值 每次存储一对值
键不能重复 ,值可以重复
方法
V put(K key,V value) 将指定的值与此映射中的指定键关联(可选操作)。
如果该键存在,则替换值,返回旧的value值,没有则返回null,
V get(Object key)返回指定键所映射的值;如果此映射不包含该键的映射关系,则返
回 null。
V remove(Object key)如果存在一个键的映射关系,则将其从此映射中移除并返回值
不存在则返回null
void clear()从此映射中移除所有映射关系
boolean containsKey(Object key) 如果此映射包含指定键的映射关系,则返回 true
boolean containsValue(Object value) 试是否存在于此映射中的值
如果此映射将一个或多个键映射到指定值,则返回 true
boolean isEmpty()如果此映射未包含键-值映射关系,则返回 true。
int size() 返回键值对的数量
遍历
Set<K> keySet()返回此映射中包含的键的 Set 视图。
将所有的key取出,放到Set集合里,然后就可以通过Set来获取值了
Set<Map.Entry<K,V>> entrySet() 返回此映射中包含的映射关系的 Set 视图
内部接口,类似内部类
public static interface Map.Entry<K,V> Map.entrySet 方法返回映射 collection 视图
返回的Set集合里面有Map.Entry<K,V>集合
Map.Entry<K,V>:在Map接口中有一个内部接口Entry
作用:当map集合一创建,那么就会在Map集合中创建一个Entry对象,用来记录
键与值(键值对对象,键与值的映射关系)
而Map.Entry接口里面提供获取键值对的方法
K getKey() 返回与此项对应的键。
V getValue() 返回与此项对应的值
HashMap<K,V>
子类
基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作
此类不保证映射的顺序 ,无序的
(除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)
注意,此实现不是同步的(多线称)
默认的初始化大小为16,每次扩充,容量变为原来的2倍
为什么容量必须是2的幂 ,
jdk 1.7
首先我们要知道,当前我们创建对象时是不会去创建桶的,也就是数组声明了但没有初始化,
当我们去put的时候,桶数组会去和一个共享的空初始数组比较,当为true,说明桶的空间还没有开辟,
这时他就会调用一个方法,将传入的对象的大小进行计算,如果不是2的幂,就将容量向上提升为2的幂
如,当为17的时候,就变为32,向上升一个幂,如果超过最大值们就用最大值
将hashcode存放在桶中,如何存放
hashcode是int修饰的,int 4个字节, 40多亿个数
如果初始桶为16,那么如何存放
大家想到的是取模 hashcode%16
那么缺点是什么呢:
负数%整数 时为负数
较慢
hashmap里使用的是(jdk1.7)先算哈希值,然后通过一个方法计算这个对象的索引下标,
那么这个方法内部做了什么呢
他使用了数组的长度减1之后也就是(length-1)和哈希值进行了按位与的操作,
并且是分布均匀的,但是如果初始容lenth不是2的幂呢
首先我们要知道,2的幂的二进制是 10,100,1000,10000...等,对它进行减1的话,得到的是一个全是1111的数
但如果不是2的幂的话,二进制就会有0的存在,此时无论和谁做按位与运算,结果都是0,那么问题就来了
这样子有一些桶就永远都是空的,因为不会被获取到下标被存入值
然后获取到下标后,将对应下标的数组元素赋值给一个Entry<k,v> ,并且判断是否为null,不为null的话
就进循环找,然后判断,通过一些什么哈希值,值得比较等,来判断是否存在,存在则覆盖返回,
不存在,则Entry<k,v>进行下一次迭代, 因为entry是一个map,是链表,所以,就这个地址一直迭代去找,就是在链表中找
最后该桶的链表查找完了,依然不存在,则添加一个在桶里,插在链表的前面,再返回null
那么扩容是怎么扩容的 ,
举例:
根据容量,默认初始16 ,乘于负载因子默认0.75
也就是说当存储的元素超过了12,那么就扩容,扩容为原来的2倍,扩容后需要重新计算哈希值,重新调整元素的位置
因为桶的数量变了,变为原来的2倍,创建新了的entry哈希桶,长度为原来的2倍
然后调用transfer方法,将原来的桶一个个的遍历出来赋值到新的桶里,注意,由于是往头插数据,所以现在就出现顺序反了的问题
问题的根源在于这个方法
问题:
容易遇到死锁 原因再多线称的情况下很有可能出现环形链表 ,
就是在做数据迁移的时候,没有考虑到迁移后的链表元素的顺序,前后元素发生改变,就是节点的next指向了原本的前一个,现在是后一个
因为顺序调换了,那么死锁问题就来了
可以通过精心构造的恶意请求引发dos
1 .8
改进 ,将数组加链表改为了 数组+链表(红黑树)
改进了扩容时的插入顺序问题 。使用的是尾插法
链表变成红黑树的时候是,符合泊松分布,那么是什么时候变为红黑树的呢,当每个桶里的元素个数分别是,0,1,2,3,4,5,6,7,8的概率
当为8的时候且桶总量超过64时,才会转红黑树,概率已经非常的小了, (桶的数量必须大于64,小于64的时候只会扩容)
hash 的实现和1.7的不同
是通过 hashCode() 的高 16 位异或低 16 位实现的:
为什么要用异或运算符?
保证了对象的 hashCode 的 32 位值只要有一位发生改变,整个 hash() 返回值就会改变。尽可能的减少碰撞。
扩容时,调用 resize() 方法,数组长度大于数组的阈值时,将 table 长度变为原来的两倍
HashMap中put方法的过程?
。调用哈希函数获取Key对应的hash值,再计算其数组下标
。 如果没有出现哈希冲突,则直接放入数组;如果出现哈希冲突,则以链表的方式放在链表后面
。如果链表长度超过阀值( TREEIFY THRESHOLD==8),就把链表转成红黑树,链表长度低于6,就把红黑树转回链表;
。如果结点的key已经存在,则替换其value即可;
。如果集合中的键值对大于数组阀值,调用resize方法进行数组扩容
数组扩容的过程
创建一个新的数组,其容量为旧数组的两倍,并重新计算旧数组中结点的存储位置。结点在新数组中的位置只有两种,原下标位置或原下标+旧
数组的大小。因为数组的扩容是 *2
使用场景:在 Map 中插入、删除和定位元素时
你知道HashMap的哈希函数怎么设计的
hash函数是先拿到通过key 的hashcode,是32位的int值,然后让hashcode的高16位和低16位进行异或操作。
尽可能降低hash碰撞,越分散越好;
为什么采用hashcode的高16位和低16位异或能降低hash碰撞?hash函数能不能直接用key的hashcode?
因为key.hashCode()函数调用的是key键值类型自带的哈希函数,返回int型散列值。int值范围为**-2147483648~2147483647**,前后加起来大
概40亿的映射空间。只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。
源码中模运算就是把散列值和数组长度-1做一个"与"操作,位运算比%运算要快。
这也正好解释了为什么HashMap的数组长度要取2的整数幂。因为这样(数组长度-1)正好相当于一个“低位掩码”。“与”操作的结果就是散列值
的高位全部归零,只保留低位值,用来做数组下标访问。
注意,在第一次put时也就是数组还为空时,数组的大小设置为了阈值的大小,阈值是在我们构造中传入的,不传默认16
但这里相信会有一点迷糊,那么容量那么大的话,为什么getsize却不是那么多,主要是内部设置了getsize是返回键值对的数目,而不是数组容
量,所以我们这里有可能会有点迷糊, 、构造有参数,则使用构造的,无参则使用默认的,反正就是传了容量大小,则计算的向上2次幂做数
组大小,没有则使用默认的16,则默认阈值为12
版本优化
扩容的时候1.7需要对原数组中的元素进行重新hash定位在新数组的位置,1.8采用更简单的判断逻辑,位置不变或索引+旧容量大小;
也就是正好是扩容2倍,1.8通过移位的方式来扩容2倍
在插入时,1.7先判断是否需要扩容,再插入,1.8先进行插入,插入完成再判断是否需要扩容;
链表的插入方式从头插法改成了尾插法,简单说就是插入时,如果数组位置上已经有元素,1.7将新元素放到数组中,原始节点作为新节点的
后继节点,1.8遍历链表,将元素放置到链表的最后;
防止发生hash冲突,链表长度过长,将时间复杂度由O(n)降为O(logn);
因为1.7头插法扩容时,头插法会使链表发生反转,多线程环境下会产生环;
A线程在插入节点B,B线程也在插入,遇到容量不够开始扩容,重新hash,放置元素,采用头插法,后遍历到的B节点放入了头部,
这样形成了环
特点
底层是哈希表,查询的速度特别快
1.8前:数组+单向链表
1.8后:数组+单向链表/红黑树(链表长度大于8)“提高查询速度
而且是无序的,指的是存储和取出的顺序
注意:之所以Map集合可以过滤相同的键,是因为key的类型要重写hashcode和equals方法
我们平常使用的包装类都是已经覆写过的,所有可以直接使用,过滤才有效
当我们要存储自定义的数据类型是,一定要覆写这两个方法,否则达不到储存效果
扩容的情况
1、 存放新值的时候当前已有元素的个数必须大于等于阈值
2、 存放新值的时候当前存放数据发生hash碰撞
数组的特点:查询效率高,插入,删除效率低。
链表的特点:查询效率低,插入删除效率高。
在HashMap底层使用数组加(链表或红黑树)的结构完美的解决了数组和链表的问题,使得查询和插入,删除的效率都很高。
该类的构造 4个
public HashMap() 无参的形式 该类内部默认的使用了,容量为16,负载因子为0.75f
public HashMap(int initialCapacity)指定初始容量,且负载因子默认的为0.75f,因为是类定义的默认值,他会调用下面的构造
public HashMap(int initialCapacity, float loadFactor) 指定容量,指定负载因子
ConcurrentHashMap 类(是 Java并发包 java.util.concurrent 中提供的一个线程安全且高效的 HashMap 实现)。
而针对 ConcurrentHashMap,在 JDK 1.7 中采用 分段锁的方式;ConcurrentHashMap中的分段锁称为Segment,
Segment继承了ReentrantLock),当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在哪一个分段中,
然后对这个分 段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。
JDK 1.8 中直接采用了CAS(无锁算法)+ synchronized。
HashMap & ConcurrentHashMap 的区别?
除了加锁,原理上无太大区别。另外,HashMap 的键值对允许有null,但是ConCurrentHashMap 都不允许。
LinkedHashMap
HashMap的子类
特点
底层是哈希表+链表(保证迭代的顺序)
是一个有序的,存储和取出的顺序一样
其他和父类一样,也是不同步的
使用场景:在需要输出的顺序和输入的顺序相同的情况下
是通过双链表表的结构来维护节点的顺序的
可以存null值。null键
那么如何实现呢 我们需要重写这个方法removeEldestEntry(Map.Entry<K,V> eldest)
这个方法是linkedhashmap的,默认返回的是false ,当我们去添加值得
覆写了afterNodeInsertion方法,在put后会去调用removeEldestEntry方法查看返回的布尔值,为true,则按照访问顺序排序,
超出了设置的缓存最大值,则删除最旧的节点对象,首节点,就一直这样,然后访问了数据后,会将数据的引用被最后面的引用,保证了一种访问顺序,
afterNodeInsertion方法是父类hashmap的
我们可以用一个类来继承该类,然后重写该方法,设置缓存的数量,当存储的数量为多少时,就开始删除老的数据
LinkedHashMap中的get方法与父类HashMap处理逻辑相似,不同之处在于增加了一处链表更新的逻辑。如果LinkedHashMap中存在要寻找的节点,那
么判断如果设置了accessOrder,则在返回值之前,将该节点移动到对应桶中链表的尾部。也就是我们说的访问顺序
或者使用构造的方式也可以,第一个参数是容器大小,第二个是加载因子,第三个则是布尔是,true为访问顺序
其他的和hashmap一致,只不过多了个双向链表
他得内部有一个类Entry<t,v>继承了HashMap.Node<k,v>内部类
且在该类里面声明了Entry<t,v> 当前类得属性 before和after,相当于两个指针
LinkedHsahmap类也定义了这个内部类的属性
transient LinkedHashMap.Entry<K,V> head;
transient LinkedHashMap.Entry<K,V> tail;
构造
public LinkedHashMap() {
super();
accessOrder = false;
}
public LinkedHashMap(int initialCapacity) {
super(initialCapacity);
accessOrder = false;
}
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
可以自定义容量大小和负载因子
还有一个参数是 accessOrder = false;
它的作用是什么呢 ,
当为false时获取的顺序则就是插入时的顺序,如果是true的话,是访问的顺序
就是访问哪个就使哪个在前面
可以用作缓存
HashTable
该类是线程安全的,内部使用的是synchronized
此类实现一个哈希表 ,类似hashMap, 哈希表也叫散列表
核心是基于哈希值得桶和列表
该哈希表将键映射到相应的值。任何非 null 对象都可以用作键或值
也就是说,键和值都不可以为空
而是hashTable是同步,说明是单线程,速度慢 ,该类是1.0版本就有的
是最早的双列集合,其他的map都是在1.2版本后才出现的
hashtable默认的初始大小为11,之后的每次扩充,容量变为u按了的2n+1,
Hashtable:底层是一个哈希表,是一个线程安全的集合
HashTable 是使用 synchronize 关键字加锁的原理(就是对对象加锁);
我们之前学的所有集合都可以存储null键,null值
但hashtable不可以 ,,注意注意,是所有的集合,list,set等,其他的map及子类
Hashtable默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。 由该方法rehash内进行的扩容,但要到数组大小超过了阈值时会扩容
负载因子为0.75 ,
创建时,如果给定了容量初始值,那么Hashtable会直接使用你给定的大小,HashMap会将其向上扩充为2的幂。也就是说Hashtable会尽量使用素数、奇
数。而HashMap则总是使用2的幂作为哈希表的大小。
扩容是key不存在时,先判断是否需要扩容,则先扩容,在插入值
之所以会有这样的不同,是因为Hashtable和HashMap设计时的侧重点不同,
hashtable的侧重点是哈希的结果更加均匀,使得哈希冲突减少,当哈希表的大小为素数时,简单的取模哈希的结果会更加均匀
而HashMap则更加关注hash的计算效率问题,HashMap为了加快hash的速度,将哈希表的大小固定为了2的幂。当然这引入了哈希分布不均匀的问题
所以HashMap为解决这问题,又对hash算法做了一些改动。 这从而导致了Hashtable和HashMap的计算hash值的方法不同
Hashtable直接使用对象的hashCode。hashCode是JDK根据对象的地址或者字符串或者数字算出来的int类型的数值。然后在对数组的长度取模。获取下标
但这种方式每一次都要做取模运算运算,效率不太好
Hashtable在计算元素的位置时需要进行一次除法运算,而除法运算是比较耗时的。
HashMap为了提高计算效率,将哈希表的大小固定为了2的幂,这样去数组下标时,不需要做除法,只需要做位运算。位运算比除法的效率要高很多。
HashMap的效率虽然提高了,但是hash冲突却也增加了。因为它得出的hash值的低位相同的概率比较高
与hashmap相比提供了
Enumeration elements = t.elements();提供了这种迭代的方式,
这种是比较老的,
Hashtable和vectir集合一样,都被其他的集合取代了
但是Hashtable的子类Properties依然很活跃
它是唯一一个和io流结合的集合
为啥Hashtable 是不允许键或值为 null 的,HashMap 的键值则都可以为 null?
这是因为Hashtable使用的是安全失败机制(fail-safe),这种机制会使你此次读到的数据不一定是最新的数据。
如果你使用null值,就会使得其无法判断对应的key是不存在还是为空,因为你无法再调用一次contain(key)来对key是否存在进行判断,
ConcurrentHashMap同理。
fail-safe是什么
快速失败(fail—fast)是java集合中的一种机制, 在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修
改),则会抛出Concurrent Modification Exception。
原理是啥?
迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。
集合在被遍历期间如果内容发生变化,就会改变modCount的值。
每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount是否为expectedmodCount值,是的话就返回遍历;否抛出异常,终止遍历。
这里异常的抛出条件是检测到 modCount!=expectedmodCount 这个条件
最后:
那你平常怎么解决这个线程不安全的问题?
java中有HashTable、Collections.synchronizedMap、以及ConcurrentHashMap可以实现线程安全的Map。
而且该collentions工具类里,来提供了synchronizedList,synchronizedMap,synchronizedCollection, 支持同步的,而这些都是内部类
HashTable是直接在操作方法上加synchronized关键字,锁住整个数组,粒度比较大,
Collections.synchronizedMap是使用Collections集合工具的内部类,通过传入Map封装出一个SynchronizedMap对象,内部定义了一个对象锁,方法内通过
对象锁实现;
ConcurrentHashMap使用分段锁,降低了锁粒度,让并发度大大提高。(1.7)
ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成,HashEntry则用于存储键值对数据,
一个ConcurrentHashMap里包含一个Segment数组,一个Segment里包含一个HashEntry,hashentry是链表
ConcurrentHashMap 底层是基于 数组 + 链表 组成的,不过在 jdk1.7 和 1.8 中具体实现稍有不同。变成了红黑树
Segment 是 ConcurrentHashMap 的一个内部类,其中 Segment 继承于 ReentrantLock。
每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。
Segment ,它的初始化容量是16
就是说如果容量大小是16他的并发度就是16,可以同时允许16个线程操作16个Segment而且还是线程安全的。
是怎么做到线程安全得呢
put逻辑
Put方法首先定位到Segment,然后在Segment里进行插入。插入操作需要经历两个步骤0,第一步判断是否需要对Segment里的HashEntry 数组
进行扩容,第二步定位添加元素的位置然后放在HashEntry数组里。
判断是否初始化,无就初始化
进行第一次key的hash来定位Segment的位置,
找到相应 的HashEntry的位置,这里会利用继承过来的锁的特性,在将数据插入指定的HashEntry位置时(链表的尾端),会通过继承
ReentrantLock的tryLock()方法尝试去获取锁,如果获取成功就直接插入相应的位置,如果已经有线程获取该Segment的锁,
那当前线程会以自旋的方式,去继续的调用tryLock()方法去获取锁,超过指定次数就挂起
size
因为并发操作,计算size的时候,还在并发的插入数据,可能会导致size和实际的size有相差,两种方案
1、第一种方案他会使用不加锁的模式去尝试多次计算ConcurrentHashMap的size,最多三次,比较前后两次计算的结果,结果一致就认为当前没
有元素加入,计算的结果是准确的
2. 第二种方案是如果第一种方案不符合,他就会给每个Segment加上锁,然后计算ConcurrentHashMap的size返回(美团面试官的问题,多个线程下
如何确定size)
get
get过程不需要加锁,除非读到的值是空的才会加锁重读为什么不需要加锁,因为使用了volatile
只需要将 Key 通过 Hash 之后定位到具体的 Segment ,再通过一次 Hash 定位到具体的元素上。
由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值。
扩容类似hashmap 1.7 ,因为现在的concurr是1.7版的
你有没有发现1.7虽然可以支持每个Segment并发访问,但是还是存在一些问题?
是的,因为基本上还是数组加链表的方式,我们去查询的时候,还得遍历链表,会导致效率很低
这个跟jdk1.7的HashMap是存在的一样问题
jdk1.7使用得是segment,1.8使用得是CAS和synchronized
1.8
ConcurrentHashMap成员变量使用volatile 修饰,免除了指令重排序,同时保证内存可见性,另外使用CAS操作和synchronized结合实现赋值操作,
多线程操作只会锁住当前操作索引的节点。
当然这种方式,键和值都不能为空
put
根据 key 计算出 hashcode 。
判断是否需要进行初始化。
1.key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。
2.然后判断是否扩容
3.在接着使用synchronized写入数据
这三个是else if的关系
判断是否需要变成红黑树
get
根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。
如果是红黑树那就按照树的方式获取值。
就不满足那就按照链表的方式遍历获取值。
他内部对节点的值和下一个节点使用了volatile ,也就是保证了可见性
并且也引入了红黑树,在链表大于一定值的时候会转换(默认是8)。
当我们在构造里指定容量时 他会先右移一位然后再加上自己再加1,然后再向上取2的幂,无参,容量默认是16,阈值也是12
有没有有序的Map?
LinkedHashMap 和 TreeMap
LinkedHashMap内部维护了一个双向单链表,有头尾节点,同时LinkedHashMap节点Entry内部除了继承HashMap的Node属性,还有before 和 after
用于
标识前置节点和后置节点。可以实现按插入的顺序或访问顺序排序。
讲讲TreeMap怎么实现有序的?
TreeMap是按照Key的自然顺序或者Comprator的顺序进行排序,内部是通过红黑树来实现。所以要么key所属的类实现Comparable接口,或者自定义
一个实现了Comparator接口的比较器,传给TreeMap用户key的比较。
TreeMap
public class TreeMap<K,V>extends AbstractMap<K,V>implements NavigableMap<K,V>, Cloneable, Serializable
基于红黑树,,此实现不是同步的