1.list和set、map区别是什么,他们分
别使用在什么场合?
底层: linkedList的底层结构实现是利用对象Node,包含pre(上一个节点),next(下一个节点)和item(存储数据内容)实现,是一个双向链表;
Linkedlist的删除和插入要比ArrayList快,只需要移动指针即可。
ArrayList和LinkedList底层实现的区别
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
list:
- 1.可以允许重复的对象。
- 2.可以插入多个null元素。
- 3.是一个有序容器,保持了每个元素的插入顺序,输出的顺序就是插入的顺序。
- 4.常用的实现类有 ArrayList、LinkedList 和 Vector。ArrayList 最为流行,它提供了使用索引的随意访问,而 LinkedList 则对于经常需要从 List 中添加或删除元素的场合更为合适。
set:
- 1.不允许重复对象
- 2.无序容器,你无法保证每个元素的存储顺序,TreeSet通过 Comparator 或者 Comparable 维护了一个排序顺序。
- 3.只允许一个 null 元素
- 4.Set 接口最流行的几个实现类是 HashSet、LinkedHashSet 以及 TreeSet。最流行的是基于 HashMap 实现的 HashSet;TreeSet 还实现了 SortedSet 接口,因此 TreeSet 是一个根据其 compare() 和 compareTo() 的定义进行排序的有序容器。而且可以重复;
map:
- 1.Map不是collection的子接口或者实现类。Map是一个接口。
- 2.Map 的 每个 Entry 都持有两个对象,也就是一个键一个值,Map 可能会持有相同的值对象但键对象必须是唯一的。
- 3.TreeMap 也通过 Comparator 或者 Comparable 维护了一个排序顺序。
- 4.Map 里你可以拥有随意个 null 值但最多只能有一个 null 键。
- 5.Map 接口最流行的几个实现类是 HashMap、LinkedHashMap、Hashtable(key不能为null,线程安全) 和 TreeMap。(HashMap、TreeMap最常用)
2.map有哪几种?
HashMap
- 最常用的Map,根据键的hashcode值来存储数据,根据键可以直接获得他的值(因为相同的键hashcode值相同,在地址为hashcode值的地方存储的就是值,所以根据键可以直接获得值),具有很快的访问速度,遍历时,取得数据的顺序完全是随机的,HashMap最多只允许一条记录的键为null,允许多条记录的值为null,HashMap不支持线程同步,即任意时刻可以有多个线程同时写HashMap,这样对导致数据不一致,如果需要同步,可以使用synchronziedMap的方法使得HashMap具有同步的能力或者使用concurrentHashMap
LinkedHashMap - 是HashMap的一个子类,但它保持了记录的插入顺序,遍历时先得到的肯定是先插入的,也可以在构造时带参数,按照应用次数排序,在遍历时会比HahsMap慢,不过有个例外,当HashMap的容量很大,实际数据少时,遍历起来会比LinkedHashMap慢(因为它是链啊),因为HashMap的遍历速度和它容量有关,LinkedHashMap遍历速度只与数据多少有关
TreeMap - 实现了sortMap接口,能够把保存的记录按照键排序(默认升序),也可以指定排序比较器,遍历时得到的数据是排过序的,Java中是用红黑树实现
HashTable - 与HashMap类似,不同的是,它不允许记录的键或值为空,支持线程同步,即任意时刻只能有一个线程写HashTable,因此也导致HashTable在写入时比较慢。
3.红黑树的优势,还有特点,旋转与着色,旋转与着色的实现过程
优势:查找效率高,O(log n)时间内做查找,n是树内的元素个数,插入和删除.
特点:
1)每个结点或红或黑
2)根结点是黑色
3)空叶子结点是黑色
4)如果一个结点是红色,那么它的子节点是黑色
5)从任意一个结点出发到空的叶子结点经过的黑结点个数相同
过程:红黑树原理
4.Hashmap的线程问题,如何解决,线程安全的Map,hashmap中为什么要用异或运算符?
HashMap线程问题:
- 1,如果两个线程put元素,如果两个key发生了hash碰撞,那么两个线程会将元素put到同一位置,hashmap是会覆盖的,这样就造成一个线程的元素丢失。
- 2,多线程下,如果两个线程同时检测到hashMap要扩容调用resize,那么一个线程进行rehash后和生成新的数组赋给底层table ,另一个线程也进行了扩容和赋值给底层table,会覆盖操作,其他线程的数据就会丢失,这样会有问题。
- 3, 多线程环境下,一个线程A,调用的get(key)方法去获取值,当要return的时候,一个线程B正好删除了这个key的value,会发生问题。
- 4,多线程环境下,当一个线程A,在循环操作hashmap的时候,另一个线程B过来删除了几个元素,这时候就会发生ConcurrentModificationException
解决方案:使用带有Synchronized的map或者使用ConrrentHashMap
为什么要用异或运算符?
- 1,Hashmap中put时寻找数组的位置时进行了运算 i =(n - 1) & hash,i为数组位置,n-1就是一个奇数,奇数的二进制最后一位肯定为1,为1的好处就是(n-1) & hash的值后一位为0或者为1,如果n不是2的整数幂,那么(n-1) & hash的运算结果后一位始终为0,这样下标结果肯定为偶数, 导致所有的数据都只能存放在偶数位置。
- 2, hashMap中的hash作了特殊处理,将key的hashCode值h进行右移16位(h >>> 16) 然后将h与右移后的h做异或运算。实际就是把一个数的低16位和高16位做异或运算。 在(n - 1) & hash 的计算中,hash变量只有末x位会参与到运算。使高16位也参与到hash的运算能减少冲突。
5.Hashmap插入一万个元素之后会不会扩容,扩容扩多少?
- 会扩容
- 扩容扩多少?
- 首先根据源码,hashMap有一个属性threshold,他的初始化值是 容量负载因子(load factor=0.75),默认容量为16,所以初始化的threshhold等于160.75=12,如图所示
- 如图为hashMap的putVal方法,也就是put方法的调用方法,可以看出当size>threshold时,进行resize()扩容,扩容为原来的2倍。
- 判断是否需要扩容:
- 扩容方法:
- 由此可以得出答案,根据扩容条件,size=10000, size>threshold(容量*0.75)进行扩容,当容量按倍数扩容到threshold为Math.pow(2, 14)*0.75>10000, 停止扩容,容量为z^14=16384。所以扩容扩16384。
6.JDK1.7和1.8中hashmap的区别?在1.7和1.8扩容有什么区别,1.8是先插入后扩容,为啥要先插入呢,先扩容不是更好?
JDK1.7和1.8中hashmap的区别?
- 实现方式
jdk版本 | 数据结构 |
1.7 | 数组+链表 |
1.8 | 数组+链表+红黑树(节点数>=8并且数组 长度>=64转换为数组+红黑树) |
- put方法
JDK1.7用的是头插法,而JDK1.8及之后使用的都是尾插法(尾插法需要遍历链表),头插法速度较高,但这种插入方法在并发场景下如果多个线程同时扩容会出现循环列表。1.8之后,链表长度到8之后就变成红黑树了,遍历的效率变高,采用尾插法效率也没多大影响,同时红黑树接口也不存在头插法了。 - 扩容
在JDK1.7的时候是先扩容后插入的,1.7只要容量达到阈值,并且桶中不为null,就进行扩容,但当key重复时,虽然桶中不为null,但实际上size还是没变,这样就发生了无效扩容;1.8是先插入再扩容的,如果插入的元素在桶中存在,则直接替换,并return,不会发生扩容,如果没有存在,不管hash有没有冲突,都插入后进行扩容;
1.7:
//扩容条件:1、当前HashMap中Entry个数 >= threshold 2、要插入位置的链表不为空
if ((size >= threshold) && (null != table[bucketIndex])) {
//扩容,新数组的长度为原数组的2倍
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
//扩容后需要重新计算index
bucketIndex = indexFor(hash, table.length);
}
1.8:
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
//替换后直接return
return oldValue;
}
}
++modCount;
//扩容条件
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
7.ConrrentHashMap的扩容过程呢?
- 什么时候会触发扩容?
- 如果新增节点之后,所在的链表的元素个数大于等于8,则会调用treeifyBin把链表转换为红黑树。在转换结构时,若tab的长度小于MIN_TREEIFY_CAPACITY,默认值为64,则会将数组长度扩容到原来的两倍,并触发transfer,重新调整节点位置。(只有当tab.length >= 64, ConcurrentHashMap才会使用红黑树。
java.util.concurrent.ConcurrentHashMap#tryPresize
putAll批量插入或者插入后发现链表长度达到8个或以上,但数组长度为64以下时触发的扩容 - 新增节点后,addCount统计tab中的节点个数大于阈值(sizeCtl),会触发transfer,重新调整节点位置,
java.util.concurrent.ConcurrentHashMap#addCount
向集合中插入新数据后更新容量计数时发现到达扩容阈值而触发的扩容 - 在
java.util.concurrent.ConcurrentHashMap#helpTransfer
扩容状态下其他线程对集合进行插入、修改、删除、合并、compute 等操作时遇到 ForwardingNode 节点时触发的扩容 - 代码解释
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
//计算每条线程处理的桶个数,每条线程处理的桶数量一样,如果CPU为单核,则使用一条线程处理所有桶
//每条线程至少处理16个桶,如果计算出来的结果少于16,则一条线程处理16个桶
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
if (nextTab == null) { // 初始化新数组(原数组长度的2倍)
try {
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
nextTab = nt;
} catch (Throwable ex) { // try to cope with OOME
sizeCtl = Integer.MAX_VALUE;
return;
}
nextTable = nextTab;
//将 transferIndex 指向最右边的桶,也就是数组索引下标最大的位置
transferIndex = n;
}
int nextn = nextTab.length;
//新建一个占位对象,该占位对象的 hash 值为 -1 该占位对象存在时表示集合正在扩容状态,key、value、next 属性均为 null ,nextTable 属性指向扩容后的数组
//该占位对象主要有两个用途:
// 1、占位作用,用于标识数组该位置的桶已经迁移完毕,处于扩容中的状态。
// 2、作为一个转发的作用,扩容期间如果遇到查询操作,遇到转发节点,会把该查询操作转发到新的数组上去,不会阻塞查询操作。
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
//该标识用于控制是否继续处理下一个桶,为 true 则表示已经处理完当前桶,可以继续迁移下一个桶的数据
boolean advance = true;
//该标识用于控制扩容何时结束,该标识还有一个用途是最后一个扩容线程会负责重新检查一遍数组查看是否有遗漏的桶
boolean finishing = false; // to ensure sweep before committing nextTab
//这个循环用于处理一个 stride 长度的任务,i 后面会被赋值为该 stride 内最大的下标,而 bound 后面会被赋值为该 stride 内最小的下标
//通过循环不断减小 i 的值,从右往左依次迁移桶上面的数据,直到 i 小于 bound 时结束该次长度为 stride 的迁移任务
//结束这次的任务后会通过外层 addCount、helpTransfer、tryPresize 方法的 while 循环达到继续领取其他任务的效果
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
while (advance) {
int nextIndex, nextBound;
//每处理完一个hash桶就将 bound 进行减 1 操作
if (--i >= bound || finishing)
advance = false;
else if ((nextIndex = transferIndex) <= 0) {
//transferIndex <= 0 说明数组的hash桶已被线程分配完毕,没有了待分配的hash桶,将 i 设置为 -1 ,后面的代码根据这个数值退出当前线的扩容操作
i = -1;
advance = false;
}
//只有首次进入for循环才会进入这个判断里面去,设置 bound 和 i 的值,也就是领取到的迁移任务的数组区间
else if (U.compareAndSwapInt(this, TRANSFERINDEX, nextIndex, nextBound = (nextIndex > stride ? nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
//扩容结束后做后续工作,将 nextTable 设置为 null,表示扩容已结束,将 table 指向新数组,sizeCtl 设置为扩容阈值
if (finishing) {
nextTable = null;
table = nextTab;
sizeCtl = (n << 1) - (n >>> 1);
return;
}
//每当一条线程扩容结束就会更新一次 sizeCtl 的值,进行减 1 操作
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
//(sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT 成立,说明该线程不是扩容大军里面的最后一条线程,直接return回到上层while循环
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
//(sc - 2) == resizeStamp(n) << RESIZE_STAMP_SHIFT 说明这条线程是最后一条扩容线程
//之所以能用这个来判断是否是最后一条线程,因为第一条扩容线程进行了如下操作:
// U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2)
//除了修改结束标识之外,还得设置 i = n; 以便重新检查一遍数组,防止有遗漏未成功迁移的桶
finishing = advance = true;
i = n; // recheck before commit
}
}
else if ((f = tabAt(tab, i)) == null)
//遇到数组上空的位置直接放置一个占位对象,以便查询操作的转发和标识当前处于扩容状态
advance = casTabAt(tab, i, null, fwd);
else if ((fh = f.hash) == MOVED)
//数组上遇到hash值为MOVED,也就是 -1 的位置,说明该位置已经被其他线程迁移过了,将 advance 设置为 true ,以便继续往下一个桶检查并进行迁移操作
advance = true; // already processed
else {
synchronized (f) {
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
//该节点为链表结构
if (fh >= 0) {
int runBit = fh & n;
Node<K,V> lastRun = f;
//遍历整条链表,找出 lastRun 节点
for (Node<K,V> p = f.next; p != null; p = p.next) {
int b = p.hash & n;
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
//根据 lastRun 节点的高位标识(0 或 1),首先将 lastRun设置为 ln 或者 hn 链的末尾部分节点,后续的节点使用头插法拼接
if (runBit == 0) {
ln = lastRun;
hn = null;
}
else {
hn = lastRun;
ln = null;
}
//使用高位和低位两条链表进行迁移,使用头插法拼接链表
for (Node<K,V> p = f; p != lastRun; p = p.next) {
int ph = p.hash; K pk = p.key; V pv = p.val;
if ((ph & n) == 0)
ln = new Node<K,V>(ph, pk, pv, ln);
else
hn = new Node<K,V>(ph, pk, pv, hn);
}
//setTabAt方法调用的是 Unsafe 类的 putObjectVolatile 方法
//使用 volatile 方式的 putObjectVolatile 方法,能够将数据直接更新回主内存,并使得其他线程工作内存的对应变量失效,达到各线程数据及时同步的效果
//使用 volatile 的方式将 ln 链设置到新数组下标为 i 的位置上
setTabAt(nextTab, i, ln);
//使用 volatile 的方式将 hn 链设置到新数组下标为 i + n(n为原数组长度) 的位置上
setTabAt(nextTab, i + n, hn);
//迁移完成后使用 volatile 的方式将占位对象设置到该 hash 桶上,该占位对象的用途是标识该hash桶已被处理过,以及查询请求的转发作用
setTabAt(tab, i, fwd);
//advance 设置为 true 表示当前 hash 桶已处理完,可以继续处理下一个 hash 桶
advance = true;
}
//该节点为红黑树结构
else if (f instanceof TreeBin) {
TreeBin<K,V> t = (TreeBin<K,V>)f;
//lo 为低位链表头结点,loTail 为低位链表尾结点,hi 和 hiTail 为高位链表头尾结点
TreeNode<K,V> lo = null, loTail = null;
TreeNode<K,V> hi = null, hiTail = null;
int lc = 0, hc = 0;
//同样也是使用高位和低位两条链表进行迁移
//使用for循环以链表方式遍历整棵红黑树,使用尾插法拼接 ln 和 hn 链表
for (Node<K,V> e = t.first; e != null; e = e.next) {
int h = e.hash;
//这里面形成的是以 TreeNode 为节点的链表
TreeNode<K,V> p = new TreeNode<K,V>
(h, e.key, e.val, null, null);
if ((h & n) == 0) {
if ((p.prev = loTail) == null)
lo = p;
else
loTail.next = p;
loTail = p;
++lc;
}
else {
if ((p.prev = hiTail) == null)
hi = p;
else
hiTail.next = p;
hiTail = p;
++hc;
}
}
//形成中间链表后会先判断是否需要转换为红黑树:
//1、如果符合条件则直接将 TreeNode 链表转为红黑树,再设置到新数组中去
//2、如果不符合条件则将 TreeNode 转换为普通的 Node 节点,再将该普通链表设置到新数组中去
//(hc != 0) ? new TreeBin<K,V>(lo) : t 这行代码的用意在于,如果原来的红黑树没有被拆分成两份,那么迁移后它依旧是红黑树,可以直接使用原来的 TreeBin 对象
ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
(hc != 0) ? new TreeBin<K,V>(lo) : t;
hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
(lc != 0) ? new TreeBin<K,V>(hi) : t;
//setTabAt方法调用的是 Unsafe 类的 putObjectVolatile 方法
//使用 volatile 方式的 putObjectVolatile 方法,能够将数据直接更新回主内存,并使得其他线程工作内存的对应变量失效,达到各线程数据及时同步的效果
//使用 volatile 的方式将 ln 链设置到新数组下标为 i 的位置上
setTabAt(nextTab, i, ln);
//使用 volatile 的方式将 hn 链设置到新数组下标为 i + n(n为原数组长度) 的位置上
setTabAt(nextTab, i + n, hn);
//迁移完成后使用 volatile 的方式将占位对象设置到该 hash 桶上,该占位对象的用途是标识该hash桶已被处理过,以及查询请求的转发作用
setTabAt(tab, i, fwd);
//advance 设置为 true 表示当前 hash 桶已处理完,可以继续处理下一个 hash 桶
advance = true;
}
}
}
}
}
}