Java集合框架
Java将优秀的算法和数据结构都封装到了集合框架之中。
数据结构考点
- 数组和链表的区别
- 链表的操作,如反转、链表环路检测、双向链表、循环链表相关操作
- 队列、栈的应用
- 二叉树的遍历方式及其递归和非递归的实现
- 红黑树的旋转
算法考点
- 内部排序,如递归排序、交换排序(冒泡、快排)、选择排序、插入排序
- 外部排序,应掌握如何利用有限的内存配合海量的外部存储来处理超大的数据集
考点扩展
- 哪些排序是不稳定的(堆排序、快排等),稳定意味着什么
- 不同数据集,各种排序最好或最差的情况
- 如何优化算法(空间换时间等思路)
Collection
-
List
-
Set
-
Queue
hashCode、compareTo、equals方法区别
HashMap、HashTable、ConcurrentHashMap
HashMap简介
- (JDK8以前,HashMap由数组+链表组成)HashMap的数组长度默认为16,数组的每个元素存储的是链表的头结点,通过hash(key.hashCode())%len函数(key的hash值,再hash取模)或者通过位运算(效率更高)来获取需要添加的元素所要存放的数组的位置。但是这种数组+链表的方式,在某个极端条件下,通过hash操作,所有的元素都存放在同一个链表中,使性能恶化(O(n))。
- (JDK8及以后,HashMap由数组+链表+红黑树组成),如果链表的长度超过TREEIFY_THRESHOLD阈值(默认为8),就会树化为红黑树;当链表长度小于UNTREEIFY_THRESHOLD(默认为6),就会再转化为链表,性能最坏为(O(logn))。
- resize()对HashMap进行扩容和初始化操作
HashMap:put方法的逻辑
-
1.如果HashMap未被初始化过,则初始化(因为HashMap使用了懒加载模式,只有使用HashMap才会初始化)。
-
2.对key求Hash值,然后再计算下标。
-
如果没有碰撞,直接放入桶(数组)中。
-
如果碰撞了,
-
以链表的方式链接到后面
-
如果链表长度超过阈值,就链表转换成红黑树。
-
(get中,如果链表长度低于6,就把红黑树转回链表)。
-
-
如果节点已经存在就替换旧值
-
如果桶满了(容量16*DEFAULT_LOAD_FACTOR=0.75f,负载因子),即当HashMap使用了75%的bucket(数组的容量),就需要调用resize()方法扩容2倍(通过新创建两倍大的bucket来替换原先的bucket)后重拍。
-
HashMap提升性能
- 减少碰撞
- 使用扰动函数,促使元素位置分布均匀,减少碰撞几率。尽量通过数组存储元素,有效提升元素访问效率
- 使用final对象,并采用合适的equals()和hashcode()方法。使用String、Integer这样的类作键比较适合,因为String是final的且实现了hashcode()和equals()方法。
HashMap扩容问题
- 多线程环境下,调整大小会存在条件竞争,容易造成死锁
- rehashing是一个比较耗时的过程
HashTable
HashTable是线程安全的,在高并发环境下锁住的是整个HashTable的对象,因此效率低下。
ConccurentHashMap
- 对HashTable进行了优化,通过锁细粒度化,将整锁拆解成多个锁进行优化。
- 早期通过分段锁Segment来实现,即将HashMap的数组拆分为多个子数组进行分段锁控制。
- 当前的ConccurentHashMap是通过CAS+synchronized使锁更细化,对每个bucket加锁,只锁住当前链表或红黑树的头结点,这样只要hash不发生碰撞,就不会产生并发,效率进一步提高。
属性方法:
- sizeCtl,由volatile修饰符进行修饰,多个线程可见。是hash表初始化和扩容时的标志位。-1表示正在进行初始化或扩容操作,-n表示n-1个线程正在进行扩容操作,正数或0代表hash表还未进行初始化,正数数值表示下一次扩容的大小。
ConccurentHashMap:put方法的逻辑
- 判断Node[]数组是否被初始化,没有则进行初始化操作
- 通过hash定位数组的索引坐标,是否有Node节点,如果没有则使用CAS进行添加(链表的头结点),添加失败则进入下一次循环
- 检查到内部正在扩容,就帮助其一起扩容
- 如果f!=null,则使用synchronized锁住f元素(链表/红黑二叉树的头元素)
- 如果是Node(链表结构)则执行链表的添加操作
- 如果是TreeNode(树型结构)则执行树添加操作
- 判断链表长度是否已经达到临界值8(默认阈值),如果超过该阈值,需要将链表进行树化
- 最后,需要将表的元素数目加1
public V put(K key, V value) {
return putVal(key, value, false);
}
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
//表示不接收空值
if (key == null || value == null) throw new NullPointerException();
//计算key的hash值
int hash = spread(key.hashCode());
int binCount = 0;
//for死循环,因为对数组元素的更新是使用CAS机制,需要不断的失败重试
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
//因为ConccurentHashMap的初始化是懒加载机制,需要判断是否为空,如果为空就对其进行初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
//如果不为空,就通过hash值找到f,f表示链表或红黑树二叉树的头结点
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//尝试通过CAS进行添加,如果操作失败就break,进行下一次的循环重试
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);//如果发现元素正在移动,就帮助其扩容
else { //如果发生碰撞,就锁住这个发生碰撞的头结点
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) { //如果是链表的头结点
if (fh >= 0) {
binCount = 1; //初始化链表的计数器
for (Node<K,V> e = f;; ++binCount) { //遍历结点,对binCount++
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val; //如果结点存在就更新对应的val
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null); //不存在就在链表尾部插入
break;
}
}
}
else if (f instanceof TreeBin) { //如果头结点为红黑二叉树结点
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value; //尝试在二叉树中添加结点
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i); //插入结点后,判断链表的结点数是否大雨阈值,对其进行树化
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount); //最后HashMap元素数目加一
return null;
}
HashMap、HashTable、ConcurrentHashMap总结
- HashMap线程不安全,数组+链表+红黑树
- Hashtable线程安全,锁住整个对象,数组+链表
- ConcurrentHashMap线程安全,CAS+同步锁,数组+链表+红黑树
- ConcurrentHashMap,首先使用无锁操作CAS插入头结点,失败循环重试
- ConcurrentHashMap,若头结点已存在,则有hash冲突,此时尝试获取头结点的同步锁,才会进行操作
- HashMap的key、value均可为null,而其他的两个类不支持