Collection
Collection集合的子接口有List,Queue,Set,它包含了这些集合的共性的方法,是单列集合父接口
List接口下的集合是有序的,允许重复,拥有索引;Set接口下的集合是无序的,不运行重复,只能有一个null值,没有索引
List接口下主要集合有ArrayList,Vector,LinkedList
Set接口下主要集合有HashSet,TreeSet,LinkedHashSet
ArrayList集合是List接口的实现类,它继承了AbstractList抽象类,实现了Iterator, RandomAccess,Cloneable,Serializable接口。它的底层是数组,数组长度可变,默认初始容量为10,扩容为原来的1.5倍,增加了0.5,是一个线程不安全的集合
ArrayList优缺点:
ArrayList底层实现了RandomArrayList接口,所以查询快
适合顺序添加元素,逆序删除元素。对于指定位置(不是末尾)添加元素性能低,因为底层添加时首先判断是否超过阈值(加载因子0.75),如果超过就扩容(相当消耗性能),然后添加时将指定位置以后的元素copy到新数组(全部向后移动一位,耗性能),删除元素时也是消耗性能。
因此ArrayList适合查询,适合顺序添加,不适合增删
ArrayList和LinkedList的区别:
- ArrayList基于数组实现,LinkedList基于链表实现
- ArrayList检索索引快,查询效率高;LinkedList要从头或从尾遍历,查询效率低
- ArrayList顺序添加快,但指定位置添加要移动元素位置,增删效率低;LinkedList添加元素不会改变元素位置,将指针指向即可,增删效率高
- ArrayList占用内存小;LinkedList占用内存大,不仅要存数据,还要存两个指针
相同:都是非线程安全的
ArrayList和Vector的区别:
相同:都实现了List接口,都是有序的,允许重复,有索引
不同:ArrayList底层没有锁,非线程安全;Vector底层用了同步锁,线程安全
ArrayList 效率高,Vector效率低
扩容方面:ArrayList增加50%,Vector增加100%
数组与集合List可以互相转换
数组转集合List:Arrays.asList(array)
集合List转数组:list.toArray()
Map
Map接口是双列集合父接口,key无序且唯一,value可以重复
Map接口下的集合主要有HashMap,LinkedHashMap,TreeMap,HashTable,concurrentHashMap
HashSet是基于HashMap实现的,HashSet的值为HashMap的key值,传入的value都是相同的虚值。在HashMap中,key值相同,后传入的value会将之前的value值覆盖,从而确保了key唯一(比较key是否相等先比较hashcode,再比较equals)
线程安全的集合类:Vector,statck,hashTable,enumeration
iterate
iterate只能单项遍历,更加安全
遍历集合时只能用iterate的remove方法,用集合的会抛concurrentModificationException异常
原因:iterate遍历集合时此时是iterate线程在执行,java不允许一个线程遍历Collection时另开辟一条线程修改它
iterate与listIterate之间的区别
- iterate可以遍历list和set接口下的集合;listiterate只能遍历list集合
- iterate只能单项遍历;listiterate可以双向遍历
- listiterate在iterate基础上增加了功能,如获取前一个元素或后一个元素索引位置,增加或替换元素
Queue
poll()和remove()区别
相同:poll()和remove()都会返回第一个元素,并将其删除
不同:如果没有该元素,poll()会返回null,remove()会抛出NoSushElementException异常
HashMap底层
对于hashcode与hash算法
hash=(h=key.hashcode)^(h>>>16)
将hashcode值与hashcode右移16位进行异或(总长度32位,右移16位正好取其半,这里非常巧妙,可以最大程度避免极端情况使其散列均匀)。这样做高16位与右移16位的全为0的高16位异或时,高16位保持不变,让低16位与右移16位后的原本高16位进行异或,目的是减少碰撞(即高16位不变,低16位与高16位进行异或结果保存在低16位中)
tab[(n-1)&hash]
将上面计算的hash值与数组长度-1进行与运算得到数组索引,相当于两者之间进行取余操作
为什么n要-1?
默认数组长度16,减一后为15二进制1111,这样与hash进行与操作时,后四位为hash本身,可以最大程度保证散列均匀(如果不减一,10000与操作,只有第五位可变,大多情况为0,并没有最大程度保证散列均匀,引起严重的hash冲突)
既然是取余,为什么要用&,而不用%?
在为2的幂次方的数时,a%b=(b-1)&a恒成立,即&运算与取余操作相等(这也解释了数组长度为什么是2的倍数)。在计算机中,都是以二进制存储,&运算效率高
为什么数组长度为2的幂次方?
1.因为2的幂次方减一之后二进制数为多个1,这样与hash进行与运算时可保证最大散列均匀,例如:10000&10110=10000
01111&10110=00110
很明显,如果不减一,无论hash值为多少,后面四位永远为0(与运算,两数都为1结果才为1,否则为0)这样大多数都是相同值(只有最前面一位是可变的,后面四位固定为0),hash冲突严重,不是我们想要的 而减1后,后面四位都是1,这样得到的结果取决于hash值,从而减少了碰撞
2.在为2的幂次方的数时,a%b=(b-1)&a恒成立,等价于取余操作
为什么要扰动2次?
加大hash值低位的随机性,使得分布更均匀,从而提高数组下标位置的随机性和均匀性,最终减少hash冲突
HashMap的put操作
- 判断键值对数组大小是否为空,是就扩容
- 根据key计算hash,对应数组索引是否没有值,没有就添加数据
- 如果有值,判断hashcode是否相等,key是否相等,如果相等,将旧的值进行替换
- 如果不相等,判断是否为红黑树类型,如果为红黑树类型,直接添加
- 不是红黑树类型,判断链表长度是否大于8,是转为红黑树类型,不是在链表添加数据,并在遍历过程中判断key是否相等,相等就替换
- 判断键值对数量是否大于容量,大于就要进行扩容
ConcurrentHashMap底层原理
分为1.7版本和1.8版本
1.7版本
用segment和HashEntry组成,每一个segment内部有一个HashEntry数组,每个HashEntry是一个链表结构的元素,segment要守护HashEntry数组,要对HashEntry进行修改,必须获得segment锁。
segment是分段后的小数组,因为segment继承了ReentrantLock,所以它是可重入锁。适用于多线程环境下,segment默认为16,可同时实现16个线程并发访问。
数组+链表结构
1.8版本
抛弃了segment分段锁,改为CAS+synchronized锁(更细粒度)
数组+链表/红黑树结构
为什么将ReentrantLock锁替换为synchronized锁?
- 在JDK1.6版本中对synchronized做了优化,会从无锁—>偏向锁—>轻量级锁—>重量级锁一步步转换
- 节省内存空间。用重入锁获取同步支持,每一个节点都要继承AQS获取同步,而正真需要的应该是头节点/根节点,这样大大浪费空间
get时为什么不加锁?
在HashEntry中,有hash,key,value,next组成,其中value和next有volatile修饰,可以保证可见性。因此get时候是可见的,而且不加锁提高了效率
get不加锁与volatile的桶数有关吗?
没有,加锁是为了扩容时保证可见性
ConcurrentHashMap的key和value为什么不能为null?
ConcurrentHashMap适用于多线程环境下。首先来谈一下为什么value不能为null,主要是有二义性。比如我们有线程a,b。让线程a去获取key的value值,结果返回null,此时不知道是key不存在而为null,还是key的value值为null,当然我们可以用containsKey来判断key值是否存在,如果返回false,即不存在,那么这个null就是key不存在;如果返回true,表明key存在。但在a线程操作后,在用containsKey之前,如果b线程执行了put操作,设置了key,且值为null,那我们并不知道这个null是值为null还是key不存在。为了解决分歧,于是value不能为null。至于为什么key不能为null,主要是作者的意思。
ConcurrentHashMap的并发度是什么?
能同时执行且不产生锁竞争的最大线程数,在1.7中默认16,如果传入值,会变为2的幂次方
1.8中抛弃了segment,并发大小依赖于数组大小
ConcurrentHashMap的迭代器是强一致性还是弱一致性?
弱一致性。就是在遍历链表/红黑树时,被遍历过的地方如果被其它线程修改了,无法察觉到,还没有遍历到的地方被修改了,可以察觉
优点:这样迭代器线程可以使用原来老的数据,而写线程也可以并发的完成改变,更重要的,这保证了多个线程并发执行的连续性和扩展性,是性能提升的关键
1.7版本与1.8版本之间的区别?
- 1.7中采用segment可重入锁,用数组+链表结构;1.8版本采用synchronized+CAS锁,用数组+链表/红黑树结构
- 锁的粒度:JDK1.7 是对需要进行数据操作的 Segment 加锁,JDK1.8 调整为对每个数组元素加锁(Node)。
- 查询时间复杂度:从 JDK1.7的遍历链表O(n), JDK1.8 变成遍历红黑树O(logN)。
HashMap与ConcurrentHashMap区别
- HashMap非线程安全,ConcurrentHashMap线程安全
- HashMap允许null值null键,ConcurrentHashMap不允许null值null键
HashMap与hashTable之间的区别
- HashMap非线程安全,没有锁,效率高,hashTable线程安全,有锁,效率低
- HashMap支持存null键null值,1个null键,多个null值;hashTable不支持,会抛异常
- 底层不同:HashMap默认容量16,扩容为原来2倍;hashTable默认长度11,扩容为原来的2n+1
- 如果给定数组大小,HashMap会变为2的幂次方,而hashTable给多少是多少
- HashMap在1.8版本超过8变红黑树,hashTable 没有
补充:
ConcurrentHashMap对整个桶进行了分割分段(segment),使用的是lock锁,比hashTable底层用的synchronized锁更加细粒度,并发性能更好 1.8版本后ConcurrentHashMap采用了CAS算法