掌握 ArrayList 扩容规则:
add规则:
初始容量为一个空的数组,elementdata中.(无参构造)
有参构造,给的就是初始化的容量initialCapacity.
还有集合。
当添加第一个元素时,会扩容。创建长度为10,新的数组替换旧的数组。继续加元素,不会扩容,直到10个元素,第二次扩容:上一次容量的1.5倍。旧数组的元素copy到新数组中,再把新元素追加到新数组中。第三次扩容:15x1.5,底层是移位来实现15>>1: 7. 7+15=22(第三次)。
addAll():
addAll(Collection c) 没有元素时,扩容为 Math.max(10, 实际元素个数),有元素时为 Math.max(原容量 1.5 倍, 实际元素个数)
扩容规则
-
ArrayList() 会使用长度为零的数组
-
ArrayList(int initialCapacity) 会使用指定容量的数组
-
public ArrayList(Collection<? extends E> c) 会使用 c 的大小作为数组容量
-
add(Object o) 首次扩容为 10,再次扩容为上次容量的 1.5 倍
-
addAll(Collection c) 没有元素时,扩容为 Math.max(10, 实际元素个数),有元素时为 Math.max(原容量 1.5 倍, 实际元素个数)
Iterator
// fail-fast 一旦发现遍历的同时其它人来修改,则立刻抛异常
Exception in thread “main” java.util.ConcurrentModificationException
刚开始list的Object有modCount也就是刚开始修改了几次,在循环迭代的过程中,改变list当中元素时,会有一个checkForComdification(),检查modCount和expectedModCount是否相等,也就是在遍历时的修改次数和最初的进行比较,若不相等,则抛出异常。
// fail-safe 发现遍历的同时其它人来修改,应当能有应对策略,例如牺牲一致性来让整个遍历运行完成,数据并不是新的了。
原来的数组复制了一份,再加1,遍历时是旧数组,添加时另一个数组。
ArrayList是fail-fast的典型代表,遍历的同时不能修改,尽快失败。
CopyOnWriteArrayList是fail-safe的典型代表,遍历的同时可以修改,原理是读写分离。
- 能够说清楚 LinkedList 对比 ArrayList 的区别,并重视纠正部分错误的认知
LinkedList
- 基于双向链表,无需连续内存
- 随机访问慢(要沿着链表遍历)
- 头尾插入删除性能高
- 占用内存多
ArrayList
- 基于数组,需要连续内存
- 随机访问快(指根据下标访问)
- 尾部插入、删除性能可以,其它部分插入、删除都会移动数据,因此性能会低
- 可以利用 cpu 缓存,局部性原理
*#randomAccess 对比随机访问性能
*#addMiddle 对比向中间插入性能
- day01.list.ArrayListVsLinkedList#addFirst 对比头部插入性能
t#addLast 对比尾部插入性能
linkedListSize 打印一个 LinkedList 占用内存
arrayListSize 打印一个 ArrayList 占用内存
局部性原理:
引入cpu缓存无论读还是写,都比直接内存中快。访问一个变量时,相邻的元素也有可能下次访问。所以先把数据一次性读入缓存。
HashMap在JDK1.7和JDK1.8中有哪些不同?HashMap的底层
实现
HashMap JDK1.8之前
JDK1.8之前采用的是拉链法。拉链法:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
HashMap JDK1.8之后
相比于之前的版本,jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。
JDK1.8主要解决或优化了一下问题:
- resize 扩容优化
- 引入了红黑树,目的是避免单条链表过长而影响查询效率,红黑树算法请参考。
- 解决了多线程死循环问题,但仍是非线程安全的,多线程时可能会造成数据丢失问题。
根据Key值,经过两次hash计算,最终得到元素在数组中的下标,若相同,则在该元素下标中链表。若积累大量元素,一般有两种思路:一、减少链表长度。二、转换为红黑树。
当元素的个数超过了初始数组的长度16的3/4时,首次扩容为上次长度2倍。
数组容量大于64,转换为红黑树。
HashMap的put方法的具体流程?
当我们put的时候,首先计算 key 的 hash 值,这里调用了 hash 方法, hash 方法实际是让key.hashCode() 与 key.hashCode()>>>16 进行异或操作,高16bit补0,一个数和0异或不变,所以 hash 函数大概的作用就是:高16bit不变,低16bit和高16bit做了一个异或,目的是减少碰撞。按照函数注释,因为bucket数组大小是2的幂,计算下标 index = (table.length - 1) & hash ,如果不做 hash 处理,相当于散列生效的只有几个低 bit 位,为了减少散列的碰撞,设计者综合考虑了速度、作用、质量之后,使用高16bit和低16bit异或来简单处理减少碰撞,而且JDK8中用了复杂度 O(logn)的树结构来提升碰撞下的性能。
1.8中先把元素加进去,再扩容的。新数组就是上次容量翻倍,再迁移。新数组代替旧数组。
不同:①:链表插入节点时,1.7是头插法,1.8是尾插法。
②:1.7是大于等于阈值且没有空位时才扩容,而1.8是大于阈值就扩容。
③:1.8在扩容计算Node索引时,会优化。
为何要用红黑树,为何一上来不树化,树化的阈值为何是8?何时会树化?何时会退化为链表?
① 红黑树用来避免DOS攻击,防止链表超长性能下降,树化应当是偶然情况。
① hash表的查找,更新的时间复杂度是o(1),而红黑树的查找,更新的时间复杂度为logn,TreeNode占用空间也比普通的Node的大,如非必要,尽量还是采用链表。
② hash值如果足够随机,则在hash表内泊松分布,在负载因子为0.75的情况下,长度超过8的链表出现的概率是6x10(-8)次方,选择8就是为了树化几率足够小。
② 树化的两个条件:链表长度超过树化阈值;数组容量>=64。
③树化的情况1:在扩容时如果拆分树时,树元素个数<=6则会退化链表。
④树化的情况2:remove树节点时,若root,root.left,root.left.left,root.right有一个为null,也会退化链表,这里是看移除之前是否为空的。
索引如何计算?hashcode都有了,为何还要提供hash方法?数组容量为何是2的n次幂?(1.8)
① 计算对象的hashCode,再进行调用HashMap的hash方法进行二次哈希,最后&(capacity-1)得到索引(前提容量必须是2的n次幂)。
②二次hash是为了综合高位数据,让哈希分布更加均匀。
③计算索引时,如果是2的n次幂可以使用位与运算代替取模,效率更高;扩容时hash&oldCap==0的元素留在原来的位置,否则新的位置=旧位置+oldCap。
④但①②③都是为了配合容量为2的n次幂时的优化手段,例如hashtable的容量就不是2的n次幂,首次扩容是10,下次扩容是上次的2倍加1,但这也有不是质数的情况,而.net是扩容后若不是质数则取下一个质数作为数组容量。并不能说哪种设计更优,应该是设计者综合了各种因素,最终选择了使用2的n次幂作为容量。
容量为质数时,hash值的分布性比较均匀。若追求更好的效率(性能)应该用2的n次幂作为数组容量,若追求更好的分布性,则用质数的数组容量。
谈一谈对ThreadLocal的理解
1.ThreadLocal可以实现资源对象的线程隔离,让每个线程各用各的资源对象,避免争用线程安全问题。
private static final ThreadLocal<Connection> t1 = new ThreadLocal();
// 到当前线程获取资源
Connection conn = t1.get();
if(conn == null){
conn = innerGetConnection(); //创建新的连接对象
t1.set(conn);
}
return conn
// 每一个线程创建时都会创建一个新的连接对象 ,线程内共享,线程间却是隔离。
2.ThreadLocal同时实现了线程内的资源共享。
ThreadLocal采用开放寻址法解决冲突。
3.其原理是,每个线程内有一个ThreadLocalMap类型的成员变量,用来存储资源对象
① 调用set方法,就是以ThreadLocal自己作为key,资源对象作为value,放入当前线程的ThreadLocalMap集合中
② 调用get方法,就是以ThreadLocal自己作为key,到当前线程中查找关联的资源值。
③调用remove方法,就是以ThreadLocal自己作为key,移除当前线程关联的资源值。
4.为什么ThreadLocalMap中的key(即ThreadLocal)要设计为弱引用?
① Thread可能需要长时间运行(如线程池中的线程),如果key不再使用,需要在内不足(GC)时释放其占用的内存。
②但GC仅是让key的内存释放,后续还要根据key是否为null来进一步释放值的内存,释放时机有:
a. 获取key发现null key:ThreadLocal在get一个值时,若这个key值不存在,则将get的key放入该下标中,value也会清理。
b. set key时,会使用启发式扫描,清除临近的null key,启发次数与元素个数,是否发现null key有关。
以上两种都是后面没有引用key了,才回收值。
c. remove时(推荐),因为一般使用ThreadLocal时把它作为静态变量(强引用),因此GC无法回收。