二、集合
1. ArrayList和LinkedList区别
- 是否线程安全:都不线程安全。
- 底层数据结构:ArrayList使用Object数组;LinkedList使用双向链表。
- 插入删除是否受元素位置影响:ArrayList:末尾追加时间复杂度为O(1),指定位置i时间复杂度为O(n-i);LinkedList:末尾追加时间复杂度近似O(1),指定位置i时间复杂度近似为O(n)。
- 是否支持快速随机访问:ArrayList支持,LinkedList不支持。
- 内存空间占用:ArrayList占用连续空间,末尾预留一定的容量空间;LinkedList链表链接,可以空间不连续,每个元素占用更多空间,因为需要额外存储前后指针变量。
RandomAccess接⼝:无定义,用于标识实现这个接口的类具有随机访问功能。
list遍历方式的选择:
- 实现了RandomAccess接口的list,优先选择普通for循环,其次foreach。
- 未实现RandomAccess接口的list,优先选择iterator遍历(foreach遍历底层也是通过iterator实现的),大size的数据,千万不要使用普通for循环。
2. ArrayList的扩容机制
https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/collection/ArrayList-Grow.md
- ArrayList的三种构造方法:
- 默认构造函数:使用初始容量10构造一个空列表。
- 带初始容量参数的构造函数:用户指定容量。
- 构造包含指定collection元素的列表,这些元素利用该集合的迭代器按顺序返回。
- 一步步分析:
- add方法先调用ensureCapacityInternal()方法。
- ensureCapacityInternal()方法调用ensureExplicitCapacity()方法。
- ensureExplicitCapacity()方法判断是否调用grow()方法。
- 如果newCapacity大于MAX_ARRAY_SIZE,则调用hugeCapacity()方法。
- System.arraycopy()和Arrays.copyOf()方法
- 联系:copyOf()内部调用了System.arraycopy()方法。
- 区别:arraycopy()需要目标数组,而且可以选择拷贝的起点和长度以及放入新数组的位置;copyOf()时系统自动在内部新建一个数组,并返回该数组。
- ensureCapacity()方法
确保ArrayList实例容量满足入参的元素数大小。
补充:
- length属性针对数组
- length()方法针对字符串
- size()方法针对泛型集合
3. HashMap和Hashtable的区别
- 线程是否安全:HashMap非线程安全,Hashtable线程安全(synchronized实现)。
- 效率:HashMap比Hashtable效率高
- 对Null key和Null value的支持:HashMap,null可以作为key,只有1个,可以有多个value为null。Hashtable不支持null key。
- 初始容量大小和每次扩容大小的不同:
- 创建时不指定容量初始值:Hashtable默认初始大小为11,之后扩充为2n+1;HashMap默认初始大小为16,扩充为2n。
- 创建时指定容量初始值:Hashtable直接使用给定值;HashMap会扩充为2的幂次方大小(HashMap中的tableSizeFor()方法保证),也就是说,HashMap总是使用2的幂次方作为哈希表的大小。
- 底层数据结构:HashMap当链表长度大于阈值(默认为8)时,将链表转化为红黑树;Hashtable没有这样的机制。
4. HashMap的底层实现:
JDK1.8之前
数组+链表,即散列表。
通过key的hashCode经过扰动函数处理过后得到hash值,然后通过(n-1)&hash判断当前元素存放的位置(n指数组的长度),如果当前位置存在元素的话,就判断hash值和key是否相同,相同直接覆盖,不同则通过拉链法解决冲突。
扰动函数指的是HashMap的hash方法,目的是减少碰撞。
// JDK1.8
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
JEDK1.8之后
增加:链表长度大于阈值时,将链表转化为红黑树
TreeMap,TreeSet和JDK1.8之后的HashMap都用到了红黑树。红黑树是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。
5. HashMap的长度为什么是2的幂次方
可以将%取余运算转化为位与操作:hash%length=hash&(length-1),提高了运算效率。
6. HashMap多线程操作导致死循环问题
并发下的Rehash会造成元素之间形成一个循环链表。jdk1.8之后已解决,但仍存在其他问题如数据丢失。
put->不存在key,addEntry->size超过阈值,resize->transfer->循环从旧表取元素放到新表->产生死循环。
7. ConcurrentHashMap和Hashtable的区别
主要体现在实现线程安全的方式上不同。
- 底层数据结构:JDK1.7的ConcurrentHashMap底层采用分段的数组+链表实现,JDK1.8则采用数组+链表/红黑树。Hashtable采用数组+链表。
- *实现线程安全的方式:
-
- JDK1.7的时候,ConcurrentHashMap(分段锁)对整个桶数组进行了分割分段(Segment),每一把锁只锁容器中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。到了JDK1.8的时候已经摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用synchronized和CAS来操作。(JDK1.6以后对synchronized锁做了很多优化)整个看起来就像是优化过且线程安全的HashMap,虽然在JDK1.8中还能看到Segment的数据结构,但是已经简化了属性,只是为了兼容旧版本
-
- Hashtable(同一把锁):使用synchronized来保证线程安全,效率非常低下。
*8. ConcurrentHashMap底层的具体实现
- HashMap实现原理
https://www.cnblogs.com/chengxiao/p/6059914.html
哈希冲突的解决方法:开放定址法,再散列函数法,链地址法。HashMap采用了链地址法,也就是数组+链表的方式。
HashMap的主干是一个Entry数组。Entry是HashMap的基本组成单元,每一个Entry包含一个key-value键值对。
在常规构造器中,没有为数组table分配内存空间(有一个入参为指定Map的构造器例外),而是在执行put操作的时候才真正构建table数组。
new HashMap -> put: inflateTable, hash, indexFor, addEntry
inflateTable: roundUpToPowerOf2, Integer.highestOneBit
确定存储位置的流程:key(hashCode()) -> hashcode(hash()) -> h(indexFor(), 即h&(length-1)) -> 存储下标
addEntry: resize -> indexFor -> createEnrty
get -> getEntry: hash, indexFor, equals - ConcurrentHashMap实现原理
JDK1.7:分段锁,继承可重入锁ReentrantLock
https://www.cnblogs.com/chengxiao/p/6842045.html
JDK1.8:CAS+synchronized,数组+链表+红黑树
https://blog.csdn.net/programmer_at/article/details/79715177
重要概念:
- table:默认为null,用来存储Node节点数据
- nextTable:默认为null,扩容是新生成的数组,其大小为原数组的两倍
- sizeCtl:默认为0,用来控制table的初始化和扩容操作
-
- -1:表示table正在初始化
-
- -N:表示有N-1个线程正在进行扩容操作
-
- 如果table未初始化,表示table需要初始化的大小
-
- 如果table初始化完成,表示table的容量,默认是table大小的0.75,计算公式为0.75*(n-(n>>>2))
- Node:保存key,value及key的hash值的数据结构。其中value和next都用volatile修饰,保证并发的可见性
- ForwardingNode:一个特殊的Node节点,hash值为-1,存储nextTable的引用。只有table发生扩容的时候,ForwardingNode才会发挥作用,作为一个占位符放在table中表示当前节点为null或已经被移动
- 实例初始化->
- table初始化(发生在第一次put操作,unsafe类的cas操作sizeCtl)->
- put(不用table(index)而是用unsafe.getObjectVolatile()来获取table对应的索引元素,是为了保证每次拿到数据都是最新的;当前bucket为空时,使用cas将Node放入对应的bucket中;如果当前map正在扩容f.hash==MOVED,则跟其他线程一起进行扩容;出现hash冲突,使用synchronized关键字;链表长度大于8时,若数组长度小于64,则扩容数组,若大于等于64,该节点链表转为红黑树)->
- treeifyBin(同一节点个数大于8个时调用)->
- tryPresize(数组长度小于64时)->
- transfer(非初始化扩容时,步长控制对CPU的使用,每个CPU最少处理16个长度的数组元素)
- TreeBin()(treeifyBin中链表转为红黑树时调用)
sun.misc.unsafe类的方法通过直接操作内存的方式来保证并发处理的安全性,使用的是硬件的安全机制。
读操作支持并发的,没有使用同步机制,也没有使用unsafe方法
多个线程如何同步处理?
主要通过synchronized和unsafe两种方式 - 在取得sizeCtl、某个位置的Node的时候,使用的都是unsafe的方法,来达到并发安全的目的。
- 当需要在某个位置设置节点的时候,则会通过synchronized的同步机制来锁定该位置的节点。
- 在数组扩容的时候,则通过处理的步长和fwd节点来达到并发安全的目的,通过设置hash值为MOVED。
- 当把某个位置的节点复制到扩张后的table的时候,也通过synchronized的同步机制来保证线程安全。
9. comparable和Comparator的区别
- comparable接口出自java.lang包,它有一个compareTo(Object obj)方法用来排序
- comparator接口出自java.util包,它有一个compare(Object obj1, Object obj2)方法用来排序