RandomAccess
接口,可以通过下标迅速获取对象;LinkedList不支持元素随机访问。Vector实现了动态数组,和ArrayList很相似。JDK需要向下兼容,因此Vector仍然可以用,但不推荐使用了。究其原因,有以下几条:
sychronized
来保证线程安全,效率相对较低,就像StringBuffer比StringBuilder效率更低一样。此外,Vector只能在尾部进行插入和删除,使其效率更低。ArrayList的扩容本质上就是先计算出新的扩容后的数组的size并实例化,然后将原数组的内容copy到新数组中去。默认情况下,扩容后的容量是原本容量的1.5倍。
HashMap的扩容机制与ArrayList类似,不过HashMap扩容后容量为原来的2倍。
在JDK1.7和JDK1.8中,HashMap的底层实现有所不同。
那么为什么不直接用红黑树呢?
因为红黑树需要左旋、右旋、变色等操作来保持平衡,而单链表不需要。当元素个数小于8个,单链表已经能够保证查询的性能。元素大于8个时,红黑树的查询时间为o(logn),远优于链表的o(n),但是会降低新增元素的效率。如果一开始就用红黑树,一方面元素太少单链表的查询性能已经够用,另一方面新增元素的速率降低,造成性能浪费。
HashMap的默认加载因子为0.75,是对时间和空间都较为平衡的选择:
resize()
进行初始化。HashMap的key通常使用String、Integer这种不可变类,String最为常用。
equals()
和hashcode()
,那么key对象对应的类就必须正确的重写这两个方法。String、Integer这些类很规范的重写了这两个方法。HashMap实现Map接口,存储键值对,通过key来计算hashcode值,速度较HashSet快。
HashSet底层其实就是HashMap,实现Set接口,存储对象,通过成员对象来计算hashcode,对象的hashcode值可能相同因此还需要equals方法。HashSet的add方法其实就是调用HashMap的put方法,存入的key是对象,value是一个始终相同虚值。
public boolean add(E e) {
return map.put(e, PRESENT)==null;// 调用HashMap的put方法,PRESENT是一个至始至终都相同的虚值
}
作者在源码中有注释,是因为泊松分布。理想状态下使用随机hash码的情况下,容器中节点的分布遵循泊松分布。链表中元素为8个时概率已经很小了,因此选用8。
ConcurrentHashMap在JDk1.7和1.8中有着不同的实现。
在JDK1.7中,ConcurrentHashMap由Segment数组结构和HashEntry组成。整个哈希表分为数多段Segment,每段多个HashEntry。其中,Segment继承自ReentrantLock,是一种可重入锁。HashEntry存储键值对。从下图可以看出,在JDK1.7中数据分段存储,每一段数据配一把锁。当一段数据被线程占用时,其他段数据可以被另外的进程访问,能够实现段之间的并发。
在JDK1.8中,ConcurrentHashMap和HashMap一样,采用数据+链表+红黑树的结构。锁的实现上,抛弃了Segment分段锁,转而采用CAS+sychronized
实现更低粒度的锁。每次只会锁住链表的头结点或者红黑树的根节点,大大提高了并发度。
在JDK1.7中,
在JDK1.8中,
CAS
方式尝试添加。f.hash = MOVED = -1
,说明别的线程在扩容,参与一起扩容。不需要;Node的元素val和指针next都被volatile
修饰,多线程环境下线程A修改val或者next时对线程B是可见的。这也是ConcurrentHashMap比其他并发集合效率高的原因,比如HashTable,用Collections.sychronized()
包装的HashMap。
其中,HashTable给整个哈希表加了一把全表锁,效率很低。Collections.sychronized()
则是使用SychronizedMap来实现的。
此外,HashEntry被volatile
修饰,是为了保证扩容时的可见性,与get不需要加锁没有关系。
不支持;会报空指针错误。
value不能为null,因为如果支持存入null,通过get(key)
得到null就不知道是key对应的value为null还是没有key。而HashMap就能存入null,这是因为HashMap可以通过containsKey(key)
来判断是否存在key。而ConcurrentHashMap使用在多线程场景下,若一开始线程A通过key获取到null是因为没有对应key,而此时线程B添加containsKey()
进行判断就会发现key存在,会造成歧义,与真实情况不符。
key不能为null,设计之初就不允许,避免可能导致的潜在错误。ConcurrentHashMap作者本身也很不喜欢集合可以存储null。在找相关资料时还看了一个很有趣的故事==>ConcurrentHashMap作者现身解释为何不允许null。
默认并发度为16,可以通过构造函数进行设置。如果设置了构造函数,会使用大于等于该值的2的次幂作为并发度。比如你设置并发度为15,则会使用16;设置17,则会使用32。
HashMap的迭代器是强一致性,修改会导致报错ConcurrentModificationException
,但是对现有key的value进行修改是允许的。
ConcurrentHashMap的迭代器是弱一致性。使用迭代器遍历所有元素,遍历过程中元素可能发生变化。如果变化发生在已遍历的部分,迭代器不会反映出来;如果变化发生在未遍历部分,迭代器就会发现并反映出来,这就是弱一致性。
Comparable
接口,并重写compareTo(T t)
方法。因为在实体类内部实现的,所以叫做内部比较器。下面的代码就表示People首先按照薪水由低到高排序,薪水一样的人就按照id从小到大排序。实现接口并重写方法的实体类可以使用Arrays.sort()
进行排序。 @Override
public int compareTo(Object o) {
People peo = (People) o;
if (salary < peo.salary) //薪水低排在前面
return -1;
if (salary > peo.salary)
return 1;
if (id < peo.id) { //id小排在前面
return -1;
}
if (id > peo.id) {
return 1;
}
return 0;
}
Comparator
接口,而实体类无需实现。因为是在实体类外部实现,所以叫外部比较器。下面的代码表示薪水由低到高进行排序。以下面比较器为例,进行People对象排序时需要用到Collections.sort(peoples, new SalaryCompare());
//外部比较类
class SalaryCompare implements Comparator {
@Override
public int compare(People o1, People o2) {
//o1 - o2是升序,o2 - o1是降序
return o1.salary - o2.salary;
}
}
set()
修改集合中的元素。fail-fast
Concurrent Modification Exception
异常。java.util包下的集合类都是快速失败,也即非线程安全。next()
或者hasNext()
之前,都会检测modCount是否为期望值。如果是,继续遍历;如果不是,抛出异常,终止遍历。modCount != exceptedmodCount
,如果集合发生变化后modCount刚好设置为expectedmodCount值,则不会抛出异常。因此,不能依据这个异常是否抛出来进行并发编程,这个异常只建议用于检测并修改bug。fail-safe
Concurrent Modification Exception
异常。