(四)常用集合与原理
1、常用List
ArrayList:底层是数组实现 Object[],线程不安全,查询和修改⾮常快,但是增加和删除慢;查询/修改多时使用;
LinkedList: 底层是双向链表 Node<E>,线程不安全,查询和修改速度慢,但是增加和删除速度快;删除/新增多时使用;
Vector: 底层是数组实现 Object[],线程安全的,操作indexof/size/remove/add等的时候使⽤synchronized进⾏加锁;已经很少使用了;
2、线程安全List
自写list:自己写个类,继承ArrayList,每个方法都加上锁;
Collections.synchronizedList(new ArrayList<>()):几乎所有的方法都加了synchronized;读写没什么区别;
CopyOnWriteArrayList<>():执行修改操作时,先获取ReentrantLock锁,然后创建一个长度+1的新书组,将数据拷贝到新数组,加入新数据,然后将原集合指向新集合,最后释放锁;
1 //CopyOnWriteArrayList 源代码
2 public boolean add(E e) {
3 final ReentrantLock lock = this.lock;
4 lock.lock();
5 try {
6 Object[] elements = getArray();
7 int len = elements.length;
8 Object[] newElements = Arrays.copyOf(elements, len + 1);
9 newElements[len] = e;
10 setArray(newElements);
11 return true;
12 } finally {
13 lock.unlock();
14 }
15 }
CopyOnWriteArrayList:读没有加锁,修改操作(add、set、 remove等)加锁,写代价较高,若复制大对象有可能发生Full GC;读写分离+最终一致;适合 少写多读 的场景;
Collections.synchronizedList:读写均加锁synchronized,写操作较多时使用;
两者相比,写性能好-Collections.synchronizedList,读性能好-CopyOnWriteArrayList;
3、ArrayList扩容机制
JDK7及之前,ArrayList创建时,默认大小是10,增加第11个时扩容,扩容为原来的1.5倍,类似饿汉式;
JDK8及之后,默认是null,无长度,增加第1个时初始化为10,类似懒汉式;
add时判断扩容流程:
第一次添加元素,先判断大小是否是0,如果是0,则扩容到10;
若元素个数大于其容量,则扩容为 原始⼤⼩+原始⼤⼩/2;
判断与扩容 实现逻辑:传入添加元素后的新容量值,原数组判空,新容量值一直取大(默认容量值、旧容量扩后值),创建新数组,拷贝数据,指针指向;
//源码入口
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
//判断是否是空的
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
//默认与传入值取大
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
//扩容后 = 原始⼤⼩+原始⼤⼩/2
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
add方法:先 判断与扩容,数组目前位置 +1 塞新值;
get方法:先判空,再判传入index是否超出范围,最后直接返回数据index位置数据;
indexOf方法:先判传参数是否是null,null是循环判==null,非空是循环equals判是否相同,返回index;
remove方法:先判空,再判传入index是否超出范围,最后 依次移动后边元素;
System.arraycopy(Object src 原数组, int srcPos 起始位置, Object dest 目标数组, int destPos 目标位置,int length 拷贝长度);
System.arraycopy(elementData, index, elementData, index+1,numMove);
4、常用map
HashMap:底层是基于数组+链表,⾮线程安全的,默认容量是16、允许有空的健和值;一般用于删除与元素定位;
Hashtable:基于哈希表实现,线程安全的(加了synchronized),默认容量是11,不允许有 null的健和值;一般不怎么用;
treeMap:使⽤存储结构是⼀个平衡⼆叉树->红⿊树,默认是生序;可以⾃定义排序规则,要实现Comparator接⼝;一般用于排序;
按照添加顺序使⽤LinkedHashMap,按照⾃然排序使⽤TreeMap,⾃定义排序 TreeMap(Comparetor c,重写compare);
ConcurrentHashMap:也是基于数组+链表,线程安全;虽然是线程安全,但是他的效率⽐Hashtable要⾼很多;
Collections.synchronizedMap():线程安全,几乎所有的方法都加了synchronized;
hashcode:顶级类Object的⽅法,所有类都是继承Object,返回是⼀个int类型的数 根据⼀定的hash规则(存储地址,字段,⻓长度等),映射成⼀个数组,即散列值;
equals:顶级类Object的⽅法,所有的类都是继承Object,返回是⼀个boolean类型 根据⾃定义的匹配规则,⽤于匹配两个对象是否⼀样;引用类型/字段匹配等;
Set,不保存重复数据,是对对应map的封装,HashSet对应的就是HashMap,treeSet对应的就是treeMap;
//HashSet源码
public HashSet() {
map = new HashMap<>();
}
private static final Object PRESENT = new Object();
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
5、map源码-HashMap与ConcurrentHashMap
HashMap底层是 数组+链表+红⿊树 (JDK8才有红⿊树,链表长度大于8,转红黑树)
Node<K,V>[] table 数组,数组每个元素都是Node的首节点,Node实现了Map.Entry<K,V>接口,每个节点都是key-value的键值对,且每个节点都指向下一个节点;
1 static class Node<K,V> implements Map.Entry<K,V> {
2 final int hash;
3 final K key;
4 V value;
5 Node<K,V> next;
.....
transient Node<K,V>[] table;
hash碰撞:不同key计算得到的Hash值相同,hashmap是链表发,要放到同个bucket中;
解决hash碰撞:链表法、开发地址法、再哈希法等
底层结构好处:
链表能解决hash冲突,将hash值相同的对象存在同⼀个链表中,并放在hash值对应的数组位;
数据较少时(少于8个),遍历线性表⽐红⿊树快;
红⿊树能提升查找数据的速度,红⿊树是平衡⼆叉树,插⼊新数据后会通过左旋,右旋、变 ⾊等操作来保持左右平衡,解决单链表查询深度的问题;
ConcurrentHashMap,在结构上无任何区别,仅仅在方法上有区别,如取spread重哈希,加锁synchronized锁,利⽤CAS获取数据;
JDK1.7: 扩容头插法,多线程同时扩容重新塞node时,易形成环;JDK1.8:扩容尾插法;
允许null值(null对应哈希是0),Integer和String更适合做key(具有不可变性),顺序与插入顺序无关,且会随着扩容而发生变化
6、map的put方法
HashMap 流程:
当前数组是否为空,空则扩容;
hash值命中的数组下标,是否空,空则直接创建节点塞值;
下标为非空,判断首节点key是否一致,一致则替换;
否则,判树形 树形插入,链表 循环判值 替换或插入 校验转红黑树;
统一 校验并扩容;
HashMap 底层源码如下:
1 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
2 boolean evict) {
3 HashMap.Node<K,V>[] tab; HashMap.Node<K,V> p; int n, i;
4 //数组判空
5 if ((tab = table) == null || (n = tab.length) == 0)
6 //扩容
7 n = (tab = resize()).length;
8 //数组hash获取下标位是否为空
9 if ((p = tab[i = (n - 1) & hash]) == null)
10 //空直接创建节点
11 tab[i] = newNode(hash, key, value, null);
12 else {
13 //非空,判断首节点是否key一致
14 HashMap.Node<K,V> e; K k;
15 if (p.hash == hash &&
16 ((k = p.key) == key || (key != null && key.equals(k))))
17 e = p;
18 //是否树结构
19 else if (p instanceof HashMap.TreeNode)
20 e = ((HashMap.TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
21 else {
22 //非树结构,循环 判一致 替换,null 新增 判长度转红黑树
23 for (int binCount = 0; ; ++binCount) {
24 if ((e = p.next) == null) {
25 p.next = newNode(hash, key, value, null);
26 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
27 treeifyBin(tab, hash);
28 break;
29 }
30 if (e.hash == hash &&
31 ((k = e.key) == key || (key != null && key.equals(k))))
32 break;
33 p = e;
34 }
35 }
36 if (e != null) {
37 V oldValue = e.value;
38 if (!onlyIfAbsent || oldValue == null)
39 e.value = value;
40 afterNodeAccess(e);
41 return oldValue;
42 }
43 }
44 //扩容
45 ++modCount;
46 if (++size > threshold)
47 resize();
48 afterNodeInsertion(evict);
49 return null;
50 }
ConcurrentHashMap:
hashtable类所有的⽅法几乎都加锁synchronized,线程安全 ⾼并发效率低;
JDK8前,ConcurrentHashMap使⽤锁分段技术,将数据分成⼀段段存储,每个数据段配置⼀把锁segment类,这个类继承ReentrantLock来保证线程安全 技术点:Segment+HashEntry;
JKD8后取消Segment,底层也是使⽤Node数组+链表+红⿊树,CAS(读)+Synchronized(写) 技术点:Node+Cas+Synchronized;
spread(key.hashCode()) 重哈希,减少碰撞概率;
tabAt(i) 获取table中索引为i的Node元素;
casTabAt(i) 利⽤CAS操作获取table中索引为i的Node元素;
ConcurrentHashMap逻辑是:
取重哈希,循环表,空表初始化;
hash值命中的数组下标,是否空,空则利用cas直接创建节点塞值;
下标为非空,判扩容 锁首节点;
判是链表,循环链表,判key是否一致,一致则替换,null则直接插入 大于8转红黑树;
否则,判树形 树形插入;
统一 校验并扩容;
ConcurrentHashMap源码如下:
1 final V putVal(K key, V value, boolean onlyIfAbsent) {
2 if (key == null || value == null) throw new NullPointerException();
3 //取重哈希
4 int hash = spread(key.hashCode());
5 int binCount = 0;
6 //直接无限循环数组
7 for (ConcurrentHashMap.Node<K,V>[] tab = table;;) {
8 ConcurrentHashMap.Node<K,V> f; int n, i, fh;
9 //空数组,初始化
10 if (tab == null || (n = tab.length) == 0)
11 tab = initTable();
12 //数组hash获取下标位是否为空
13 else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
14 //利用cas出入节点
15 if (casTabAt(tab, i, null, new ConcurrentHashMap.Node<K,V>(hash, key, value, null)))
16 break;
17 }
18 //判断是否需要先扩容
19 else if ((fh = f.hash) == MOVED)
20 tab = helpTransfer(tab, f);
21 else {
22 V oldVal = null;
23 //hash冲突,加锁
24 synchronized (f) {
25 if (tabAt(tab, i) == f) {
26 //是链表,循环
27 if (fh >= 0) {
28 binCount = 1;
29 for (ConcurrentHashMap.Node<K,V> e = f;; ++binCount) {
30 K ek;
31 if (e.hash == hash &&
32 ((ek = e.key) == key ||
33 (ek != null && key.equals(ek)))) {
34 oldVal = e.val;
35 if (!onlyIfAbsent)
36 e.val = value;
37 break;
38 }
39 ConcurrentHashMap.Node<K,V> pred = e;
40 if ((e = e.next) == null) {
41 pred.next = new ConcurrentHashMap.Node<K,V>(hash, key,
42 value, null);
43 break;
44 }
45 }
46 }
47 //是树
48 else if (f instanceof ConcurrentHashMap.TreeBin) {
49 ConcurrentHashMap.Node<K,V> p;
50 binCount = 2;
51 if ((p = ((ConcurrentHashMap.TreeBin<K,V>)f).putTreeVal(hash, key,
52 value)) != null) {
53 oldVal = p.val;
54 if (!onlyIfAbsent)
55 p.val = value;
56 }
57 }
58 }
59 }
60 if (binCount != 0) {
61 if (binCount >= TREEIFY_THRESHOLD)
62 treeifyBin(tab, i);
63 if (oldVal != null)
64 return oldVal;
65 break;
66 }
67 }
68 }
69 //扩容
70 addCount(1L, binCount);
71 return null;
72 }
浙公网安备 33010602011771号