为什么不用HashTable?
虽然HashTable是线程安全的,里面的每个方法都有Synchronized,但是正是这样极大地增加了开销,因为锁住了整个HashTable,所以在多线程里面使用ConCurrentHashMap来代替,而且HashTable的key和value都不能为null 而HashMap可以存储null键和null值,虽然不是线程安全的,但是在单线程环境下速度比HashTable快得多,而多线程下有ConcurrentHashMap更优,所以HashTable便没有使用的地方了,并且HashMap的查询速率特别快,几乎可以达到O(1)
HashMap的实现原理?
HashMap基于hashing原理,我们通过put()和get()方法储存和获取对象。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,让后找到bucket位置来储存值对象。当获取对象时,先通过key的hashcode找到对应的bucket,然后通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用链表来解决碰撞问题,当发生碰撞了,对象将会储存在链表的下一个节点中。 HashMap在每个链表节点中储存键值对对象。
HashMap中null可以为键,但只能有一个null键,但是可以有多个null为值,不能通过get()方法来判断HashMap中是否存在某个key,因为返回null时既能表示没有该key,也能表示一个key的value是null。应该使用containsKey()来判断。
ConcurrentHashMap知识点?
ConcurrentHashMap是线程安全的,并且采用了分段锁的策略,把整个map分成N个Segment(Segment是一个可重入锁),这样操作时仅仅锁住了操作的那一段,这样极大地提高了效率,而不像HashTable一样锁住整个map,并且ConcurrentHashMap的读操作没有加锁,可以并发地读,又因为value变量是volatile的,所以保证能看到最新值。ConcurrentHashMap插入前会检测需不需要扩容,以避免无效扩容
总结:HashMap和HashTable基于的类不同,HashTable更古老,因此考虑到的并没有HashMap周到
HashTable和HashMap的迭代器区别?
HashMap的迭代器是fail-false迭代器(Iterator) 而HashTable是enumerator。 Iterator迭代器在其他线程修改该迭代器所迭代的对象的结构时会抛出异常,但是迭代器本身的remove()方法不会 (也可以理解为Iterator和enumerator的区别)
不同的锁机制?
HashTable一次锁住整个hash表,ConcurrentHashMap一次锁住一个桶
ConcurrentHashMap将hash表分为16个桶,使用get,put,remove时只锁住当前需要的桶。16个桶只是初始的,后面可以增加桶的数量
HashMap为什么不是线程安全的?
HashMap底层是一个Entry数组,当发生hash冲突的时候,HashMap采用链表的方式来解决,在对应的数组位置存放链表的头结点。对链表而言,新加入的节点会从头结点假如
1)在put操作时,如果两个线程同时对同一个数组的位置调用addEntry(),两个线程会同时得到现在的头结点,这样的话其中一个线程的写入操作就会覆盖另一个线程的写入操作
2)当加入键值对时如果容量超过那个负载因子,就会调用resize操作。这个操作会生成一个新的容量的数组,然后对原数组的所有键值对重新计算和写入新的数组。在多线程环境下,存在同时其他的元素也在进行put操作,如果hash值相同,可能出现在同一个数组下造成闭环,导致get时出现死循环
为什么ConcurrentHashMap是线程安全的?
jdk1.7中,ConcurrentHashMap使用了锁分段技术来实现线程安全,jdk1.8中放弃了锁分段的做法,采用CAS和synchronized方式并发处理。
为什么hashMap扩容要寻找建立新的数组?
建立新的数组后将使元素更加的分散,这样就减少了出现哈希冲突的几率
map和hashmap?
HashMap的插入和读的时间复杂度都O(1),是这样保证的:会首先计算出key的hash值,然后找到对应的位置,进行插入和读操作,但是这里有一个问题就是假如出现了hash冲突,那么就是链表了,而链表就不能保证O(1)的复杂度,那么为什么还说hashMap的时间复杂度是O(1)呢?因为现在的hash冲突已经很少了,并且O(1)是理想情况下的,这种理想状态需要设计者去保证,所以其实说O(1)是利用了数组结构,使得key的查找在O(1)时间内完成。
hashMap的底层是哈希表,而map的底层是红黑树。hashMap的内部不是排序的,map的内部是排序的,hashMap的时间复杂度是O(1),map的时间复杂度是O(logn)
concurrentHashMap的底层原理(1.8)?
concurrentHashMap的数据结构是(数组+链表+红黑树)
Node的数据结构简单,是一个链表,但是只允许查找,不允许修改
TreeNode继承自Node,数据结构是二叉树,用于红黑树中存储结构
concurrentHashMap的初始化是空实现,初始化操作是在put操作中实现的
concurrentHashMap的put操作:1.如果没有初始化就调用initTable()方法进行初始化
2.如果没有hash冲突则直接CAS插入
3.如果正在进行扩容,那么先进行扩容操作
4.如果存在hash冲突,那么加锁来保证的线程安全(两种情况:一种是链表直接遍历到尾段进行插入,一种是按照红黑树的结构进行插入)
5.如果链表的数量大于8,会先转换成红黑树,break后再一次进入循环
6.如果调用成功就调用addCount()方法统计size,并且检查是否需要扩容
put过程的总结:并发处理时使用的是乐观锁,当有冲突时才进行并发处理
concurrentHashMap的get操作: 1.计算hash值,定位到该table索引的位置,如果首节点符合就直接返回
3.如果不符合的话,继续向下遍历节点,匹配就返回,否则继续遍历直到遍历到底还是不符合的话就返回null
concurrentHashMap支持null键和null值吗?
都不支持。concurrentHashMap不能put null是因为无法分辨key是没找到还是有key但是值为null,这在多线程里面是模糊不清的,所以直接不让put null
hashMap(1.7与1.8版本)?
1.7版本的hashMap使用的是头插法,但是头插法在多线程的情况下容易形成死循环,并且此时hashMap的底层结构是数组+链表
1.8版本的hashMap使用的是尾插法,底层结构也变成了数组+链表+红黑树,所以复杂度也从O(n)变成了O(logn)
链表为什么节点为8时变为红黑树,6时还原为链表?
这是设计者通过泊松分布所得到的结论,首先,链表的复杂度为O(n),红黑树的复杂度为O(logn),所以当节点为8时,这个时候使用链表的话平均查找长度为4,而红黑树log(8)为3。中间有个值7是为了防止链表和红黑树之间频繁地转换,并且treenode的大小是node的两倍,所以为了平衡空间和时间的关系,所以选择6变回链表
HashMap和TreepMap?
HashMap和TreeMap都是非线程安全的。hashMap中通过hashcode对内容进行快速查找,TreeMap中所有的元素都保持着固定的顺序,如果想要得到一个有序的结果可以采用TreeMap。HashMap基于哈希表实现,TreeMap基于红黑树实现的。HashMap适用插入和删除,TreepMap适用于自然顺序或自定义顺序遍历key,总体上HashMap更快一些,如果要排序再用TreepMap(因为TreeMap需要维护平衡)
hashMap和LinkedHashMap的区别?
1.LinkedHashMap继承自hashMap,基于hashMap和双向链表实现
2.hashMap是无序的,LinkedHashMap有序,可以分为插入顺序和访问顺序两种,默认是插入顺序。如果是访问顺序,那put和get操作已经存在的entry时,都会把entry移动到双向链表的表尾(也就是先删除再插入)
3.LinkedHashMap存储获取数据还是跟hashMap一样的都是使用entry[]数组的方式。只是用了双向链表来保证顺序
ConcurrentHashMap的size操作?
jdk1.7中,先不加锁计算三次,如果三次的结果都一样,则不用进行加锁,如果三次结果不一致,再加锁
jdk1.8:由于size()方法可能会返回int的最大值,所以一般使用mappingCount()方法。他们都会调用一个方法,sumCount(),其实这个方法才是真正的size()方法,有两个属性baseCount和CounterCells,假如最后CounterCells不为空,那么size就为baseCount+CounterCells。其中baseCount是一个volatile变量,在进行remove或是put方法后会更改这个变量,而CounterCells主要是在CAS修改失败后,利用CAS修改CounterCells的值,而在fullAddCount方法中会一直循环直到操作成功,所以size的值就是CounterCells+baseCount的值
HashMap为什么会发生死循环(1.7)?
因为当多个线程同时执行put操作时,假如这个这个要put的key的hashcode是相同的,那么就会被放到同一个链表下,假如这个时候是A.next = B,但是进行重新计算位置并放入新的table里是是用的头插法,即这个时候B.next=A,但是另一个线程在执行这个操作时可能看到的是A.next=B,这样的话A.next=B,B.next=A,就会形成死循环
HashSet?
HashSet简单的理解就是HashSet对象中不能存储相同的数据,存储数据时是无序的。但是HashSet存储元素的顺序并不是按照存入时的顺序(和List显然不同) 是按照哈希值来存的所以取数据也是按照哈希值取得,实现了set
- 它存储唯一元素并允许空值
- 它由HashMap支持
- 它不保持插入顺序
- 它不是线程安全的
HashSet如何保持唯一性?
当我们将一个对象放入一个HashSet时,它使用该对象的hashcode值来确定一个元素是否已经在该集合中。
每个散列码值对应于某个块位置,该块位置可以包含计算出的散列值相同的各种元素。但是具有相同hashCode的两个对象可能不相等。
因此,将使用equals()方法比较同一存储桶中的对象。
HashSet的性能
HashSet的性能主要受两个参数影响 - 初始容量和负载因子。
将元素添加到集合的预期时间复杂度是O(1),在最坏的情况下(仅存在一个存储桶)可以降至O(n) - 因此,维护正确的HashSet容量至关重要。
一个重要的注意事项:从JDK 8开始,最坏的情况时间复杂度为O(log * n)。
负载系数描述了最大填充级别,在该级别之上,需要调整一组的大小。
我们还可以创建一个HashSet,其中包含初始容量和加载因子的自定义值: